Files
enterprise-addons/sale_subscription/controllers/portal.py
T

662 lines
35 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import werkzeug
from collections import OrderedDict
from dateutil.relativedelta import relativedelta
from math import ceil
from werkzeug.urls import url_encode
from odoo import Command, fields, http, _, SUPERUSER_ID
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
from odoo.http import request
from odoo.tools import format_date, str2bool
from odoo.tools.misc import get_lang
from odoo.addons.sale.controllers import portal as payment_portal
from odoo.addons.payment import utils as payment_utils
from odoo.addons.portal.controllers.portal import pager as portal_pager
from odoo.addons.sale.controllers import portal as sale_portal
from odoo.addons.sale_subscription.models.sale_order import SUBSCRIPTION_PROGRESS_STATE, SUBSCRIPTION_CLOSED_STATE
class CustomerPortal(payment_portal.PaymentPortal):
def _get_subscription_domain(self, partner):
# Include all subscriptions linked to any contact (child) of the company (commercial partner).
# This ensures all users of a company can view and manage each other's subscriptions.
return [
('partner_id', 'child_of', partner.commercial_partner_id.id),
('subscription_state', 'in', ['3_progress', '4_paused', '6_churn']),
('is_subscription', '=', True)
]
def _prepare_home_portal_values(self, counters):
""" Add subscription details to main account page """
values = super()._prepare_home_portal_values(counters)
if 'subscription_count' in counters:
if request.env['sale.order'].has_access('read'):
partner = request.env.user.partner_id
values['subscription_count'] = request.env['sale.order'].search_count(self._get_subscription_domain(partner))
else:
values['subscription_count'] = 0
return values
def _get_subscription(self, access_token, order_id):
logged_in = not request.env.user.sudo()._is_public()
order_sudo = request.env['sale.order']
try:
order_sudo = self._document_check_access('sale.order', order_id, access_token)
except AccessError:
if not logged_in:
subscription_url = '/my/subscriptions/%d' % order_id
return order_sudo, werkzeug.utils.redirect('/web/login?redirect=%s' % werkzeug.urls.url_quote(subscription_url))
else:
raise werkzeug.exceptions.NotFound()
except MissingError:
return order_sudo, request.redirect('/my')
return order_sudo, None
def _get_invoice_sudo(self, access_token, invoice_id):
try:
invoice_sudo = self._document_check_access('account.move', invoice_id, access_token)
except AccessError:
raise ValidationError(_('Invalid access token.'))
except MissingError:
raise ValidationError(_('Invoice not found.'))
return invoice_sudo
@http.route(['/my/subscriptions', '/my/subscriptions/page/<int:page>', '/my/subscription'], type='http', auth="user", website=True)
def my_subscription(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, order_id=None, **kw):
values = self._prepare_portal_layout_values()
partner = request.env.user.partner_id
Order = request.env['sale.order']
if order_id:
order_sudo, redirection = self._get_subscription(None, int(order_id))
if redirection:
return redirection
origin_order = order_sudo.origin_order_id or order_sudo
domain = [('state', 'not in', ['cancel', 'draft']),
'|', ('id', '=', origin_order.id), ('origin_order_id', '=', origin_order.id)
]
else:
domain = self._get_subscription_domain(partner)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'},
'name': {'label': _('Name'), 'order': 'name asc, id asc'},
'subscription_state': {'label': _('Status'), 'order': 'subscription_state asc, id desc'}
}
searchbar_filters = {
'all': {'label': _('All'), 'domain': []},
'open': {'label': _('In Progress'), 'domain': [('subscription_state', 'in', ['3_progress', '4_paused'])]},
'to_renew': {'label': _('To Renew'), 'domain': [('subscription_state', '=', '3_progress'), ('next_invoice_date', '<', fields.Date.today())]},
'close': {'label': _('Closed'), 'domain': [('subscription_state', '=', '6_churn')]},
}
# default sort by value
if not sortby:
sortby = 'subscription_state'
order = searchbar_sortings[sortby]['order']
# default filter by value
if not filterby:
filterby = 'all'
domain += searchbar_filters[filterby]['domain']
# pager
order_count = Order.search_count(domain)
pager = portal_pager(
url="/my/subscriptions",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
total=order_count,
page=page,
step=self._items_per_page
)
orders = Order.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
request.session['my_subscriptions_history'] = orders.ids[:100]
request.session['current_history'] = 'my_subscriptions_history'
values.update({
'subscriptions': orders if not order_id else orders.with_context(force_sale_url=True),
'page_name': 'subscription',
'pager': pager,
'default_url': '/my/subscriptions',
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
})
return request.render("sale_subscription.portal_my_subscriptions", values)
@http.route()
def portal_my_quotes(self, **kwargs):
request.session['current_history'] = 'my_quotations_history'
return super().portal_my_quotes(**kwargs)
@http.route()
def portal_my_orders(self, **kwargs):
request.session['current_history'] = 'my_orders_history'
return super().portal_my_orders(**kwargs)
@http.route(['/my/subscriptions/<int:order_id>', '/my/subscriptions/<int:order_id>/<access_token>',
'/my/subscription/<int:order_id>', '/my/subscription/<int:order_id>/<access_token>'],
type='http', auth='public', website=True)
def subscription(self, order_id, access_token=None, message='', report_type=None, download=False, payment_amount=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if report_type in ('html', 'pdf', 'text'):
return self._show_report(model=order_sudo, report_type=report_type, report_ref='sale.action_report_saleorder', download=download)
payment_amount = self._cast_as_float(payment_amount)
enable_token_management = request.env.user.partner_id in (order_sudo.partner_id.child_ids | order_sudo.partner_id)
closable = order_sudo.user_closable and order_sudo.subscription_state in ['3_progress', '4_paused']
display_close = closable and (not order_sudo.end_date or order_sudo.end_date > order_sudo.next_invoice_date)
# day periods are not supported in standard but may be used by the community.
# Invoicing more than once a day will never be supported because of the invoicing cron periodicity.
periods = {'day': 'days', 'week': 'weeks', 'month': 'months', 'year': 'years'}
# Calculate the duration when the customer can reopen his subscription
missing_periods = 1
if order_sudo.next_invoice_date:
rel_period = relativedelta(datetime.datetime.today(), order_sudo.next_invoice_date)
# In missing_periods, include current period to ensure not show payment form to portal if more than one billing cycle is overdue
missing_periods = ceil(getattr(rel_period, periods[order_sudo.plan_id.billing_period_unit]) / order_sudo.plan_id.billing_period_value) + 1
action = request.env.ref('sale_subscription.sale_subscription_action')
token_management_url_params = {
'manage_subscription': True,
'sale_order_id': order_id,
'access_token': access_token,
}
progress_child = order_sudo.subscription_child_ids.filtered(lambda s: s.subscription_state in SUBSCRIPTION_PROGRESS_STATE)
# prevent churned SO with a confirmed renewal to be reactivated. The child should be updated.
# Additionally, prevent customers from reopening any closed subscription by paying either
# the plan or the subscription plan is archived.
display_payment_message = False
if order_sudo.subscription_state in ['3_progress', '4_paused'] and not progress_child:
# In progress orders are always payable.
display_payment_message = True
elif order_sudo.subscription_state in SUBSCRIPTION_CLOSED_STATE and not progress_child:
# The SO is payable if it has an active plan and products
active_products = all(prod.active for prod in order_sudo.order_line.product_template_id)
display_payment_message = order_sudo.plan_id.active and active_products
backend_url = f'/odoo/action-{order_sudo._get_portal_return_action().id}/{order_sudo.id}'
portal_page_values = {
'page_name': 'subscription',
'sale_order': order_sudo,
'report_type': 'html',
'display_close': display_close,
'closable': closable,
'close_reasons': request.env['sale.order.close.reason'].search([]),
'missing_periods': missing_periods,
'user': request.env.user,
'is_salesman': request.env.user.has_group('sales_team.group_sale_salesman'),
'action': action,
'message': message,
'pricelist': order_sudo.pricelist_id.sudo(),
'enable_token_management': enable_token_management,
'token_management_url': f'/my/payment_method?{url_encode(token_management_url_params)}',
'payment_action_id': request.env.ref('payment.action_payment_provider').id,
'display_payment_message': display_payment_message,
'backend_url': backend_url,
'product_documents': order_sudo._get_product_documents(),
'next_billing_details': order_sudo._next_billing_details(),
'format_date': lambda date: format_date(request.env, date),
'next_invoice_date_at_resume': max(order_sudo.user_pause_start, fields.Date.today()) if order_sudo.user_pause_start else False,
'is_subscription_postpaid': order_sudo._is_subscription_postpaid(),
'is_delivery_product': int(any(line.product_id.invoice_policy == 'delivery' for line in order_sudo.order_line)),
**self._prepare_partner_addresses(order_sudo)
}
history_session_key = request.session.get('current_history', 'my_subscriptions_history')
portal_page_values = self._sale_order_get_page_view_values(
order_sudo, access_token, portal_page_values, history_session_key, **kw)
payment_form_values = {
'default_token_id': order_sudo.payment_token_id.id,
'sale_order_id': order_sudo.id, # Allow Stripe to check if tokenization is required.
}
payment_context = {
'partner_id': order_sudo.partner_id.id,
}
rendering_context = {
**SalePortal._get_payment_values(self, order_sudo, is_subscription=True, subscription_anticipate=True, payment_amount=payment_amount),
**portal_page_values,
**payment_form_values,
**payment_context,
}
return request.render("sale_subscription.subscription_portal_template", rendering_context)
def _prepare_partner_addresses(self, order_sudo):
""" Retrieves the invoicing and delivery addresses of the sale order, and format them properly for display. """
addresses_data = self._prepare_address_data(order_sudo.partner_id)
check_user_id = order_sudo.user_id or request.env.user
# we don't allow to update the subscription shipping address yet.
return {
'multiple_addresses_enabled': check_user_id.has_group('account.group_delivery_invoice_address'),
'invoicing_addresses': [{'id': partner.id, 'address': f'{partner.name}, {partner._display_address(without_company=True)}'} for partner in addresses_data.get('billing_addresses')],
'delivery_addresses': [{'id': partner.id, 'address': f'{partner.name}, {partner._display_address(without_company=True)}'} for partner in addresses_data.get('delivery_addresses') if order_sudo.partner_shipping_id.id == partner.id],
}
@http.route([
'/my/orders/<int:order_id>/document/<int:document_id>',
'/my/subscriptions/<int:order_id>/document/<int:document_id>'])
def portal_quote_document(self, *args, **kwargs):
return super().portal_quote_document(*args, **kwargs)
@http.route(['/my/subscriptions/<int:order_id>/close', '/my/subscription/<int:order_id>/close'], type='http', methods=["POST"], auth="public", website=True)
def close_account(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if order_sudo.plan_id.user_closable:
close_reason = request.env['sale.order.close.reason'].browse(int(kw.get('close_reason_id')))
if close_reason:
if kw.get('closing_text'):
order_sudo.message_post(body=_('Closing text: %s', kw.get('closing_text')))
order_sudo.with_context(allow_future_end_date=True).set_close(close_reason_id=close_reason.id)
return request.redirect(f'/my/subscriptions/{order_id}?access_token={access_token}')
@http.route(['/my/subscriptions/<int:order_id>/change_plan'], type='http', methods=["POST"], auth="public", website=True)
def change_plan(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if order_sudo.plan_id.related_plan_id and order_sudo._can_be_edited_on_portal():
if new_plan := request.env['sale.subscription.plan'].browse(int(kw.get('plan_id'))):
order_sudo.plan_id = new_plan
return request.redirect(order_sudo.get_portal_url())
@http.route(['/my/subscriptions/<int:order_id>/pause'], type='http', methods=["POST"], auth="public", website=True)
def subscription_pause(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if order_sudo.next_invoice_date <= fields.Date.today():
raise UserError(self.env._('You cannot pause a subscription that is not yet due.'))
if order_sudo._is_subscription_postpaid():
raise UserError(self.env._('You cannot pause a subscription with postpaid lines.'))
until = kw.get('until')
if not until:
raise ValidationError(self.env._('Please select a date to pause the subscription until.'))
if order_sudo.plan_id.pausable_by_user and until and not order_sudo.user_pause_start:
user_lang = get_lang(request.env).date_format
until = fields.Date.to_date(datetime.datetime.strptime(until, user_lang).date())
user_pause_start = order_sudo.next_invoice_date
if until == order_sudo.next_invoice_date:
raise UserError(self.env._('You cannot pause the subscription for 0 days.'))
if until < order_sudo.next_invoice_date:
raise UserError(self.env._('A pause can\'t take place before the next invoice date.'))
if until > user_pause_start + order_sudo.plan_id.billing_period:
raise UserError(self.env._('Pause duration cannot exceed 1 period.'))
message_body = self.env._(
"Subscription will be paused from %(date_from)s to %(date_to)s.",
date_from=format_date(request.env, user_pause_start),
date_to=format_date(request.env, until),
)
user = request.env['res.users'].sudo().search([('partner_id', '=', order_sudo.partner_id.id)], limit=1) or SUPERUSER_ID
order_sudo.with_user(user).sudo().with_context(subscription_pause=True).write({
'next_invoice_date': until,
'user_pause_start': user_pause_start,
})
order_sudo.with_user(user).sudo().message_post(body=message_body)
return request.redirect(order_sudo.get_portal_url())
@http.route(['/my/subscriptions/<int:order_id>/resume'], type='http', methods=["POST"], auth="public", website=True)
def subscription_resume(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if order_sudo.next_invoice_date <= fields.Date.today():
raise UserError(self.env._('You cannot resume a subscription that is not yet due.'))
if order_sudo.plan_id.pausable_by_user and order_sudo.user_pause_start:
user = request.env['res.users'].sudo().search([('partner_id', '=', order_sudo.partner_id.id)], limit=1) or SUPERUSER_ID
invoice_date = max(order_sudo.user_pause_start, fields.Date.today())
message_body = self.env._("Subscription was resumed on the %s.", format_date(request.env, invoice_date))
order_sudo.with_user(user).sudo().with_context(subscription_pause=True).write({
'next_invoice_date': invoice_date,
'user_pause_start': False
})
order_sudo.with_user(user).sudo().message_post(body=message_body)
return request.redirect(order_sudo.get_portal_url())
@http.route(['/my/subscriptions/<int:order_id>/upsell'], type='http', auth="public")
def subscription_portal_upsell(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
if order_sudo.user_quantity:
upsell = order_sudo._create_renew_upsell_order('7_upsell', _('An upsell has been created by the client.'))
upsell.action_quotation_sent()
return request.redirect(upsell.get_portal_url())
@http.route(['/my/subscriptions/<int:order_id>/renewal'], type='http', auth="public")
def subscription_portal_renewal(self, order_id, access_token=None, change_plan=False, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
qs = ""
if change_plan:
qs = "&change_plan=true"
if order_sudo.user_extend:
renewal = order_sudo._create_renew_upsell_order('2_renewal', _('A renewal has been created by the client.'))
renewal.action_quotation_sent()
return request.redirect(renewal.get_portal_url(query_string=qs))
@http.route(['/my/subscriptions/<int:order_id>/change_address'], type='http', methods=["POST"], auth="public", website=True)
def subscription_change_address(self, order_id, access_token=None, **kw):
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
return redirection
check_user_id = order_sudo.user_id or request.env.user
multiple_addresses_enabled = check_user_id.has_group('account.group_delivery_invoice_address')
if not multiple_addresses_enabled:
raise request.not_found()
addresses_data = self._prepare_address_data(order_sudo.partner_id)
invoicing_id = int(kw.get('invoicing_address', 0))
invoicing_partner = request.env['res.partner'].browse(invoicing_id)
# Check if the provided address is in the list of accessible address to the SO partner
if invoicing_partner in addresses_data['billing_addresses']:
order_sudo.partner_invoice_id = invoicing_partner
return request.redirect(order_sudo.get_portal_url(query_string="&%s" % url_encode({'address_updated': 'true'})))
class PaymentPortal(payment_portal.PaymentPortal):
def _get_extra_payment_form_values(
self, manage_subscription=False, sale_order_id=None, subscription_invoice_id=None, access_token=None, **kwargs
):
""" Override of payment: additional rendering values for subscription order management.
Sale order transaction for subscription: We need to assign the token once the payment is done.
Invoice of a subscription: we update the transaction route and define an alternative transaction_route_subscription
that will be used if the customer check the option to save that payment method on the contract.
:param bool manage_subscription: Whether the payment form should be adapted to allow
managing subscriptions. This allows distinguishing cases.
:param str sale_order_id: The sale order for which a payment is made, as a `sale.order` id.
:param str subscription_invoice_id: The account move for which a payment is made, as a `account.move` id.
:param str access_token: The access token of the subscription.
:param dict kwargs: Locally unused keywords arguments.
:return: The dict of extra payment form values.
:rtype: dict
"""
extra_payment_form_values = super()._get_extra_payment_form_values(
manage_subscription=manage_subscription,
sale_order_id=sale_order_id,
subscription_invoice_id=subscription_invoice_id,
access_token=access_token,
**kwargs,
)
if sale_order_id:
sale_order_id = self._cast_as_int(sale_order_id)
extra_payment_form_values.update({
'sale_order_id': sale_order_id, # Allow Stripe to check if tokenization is required.
})
try: # Check document access against what could be a portal access token.
order_sudo = self._document_check_access('sale.order', sale_order_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
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id)
if order_sudo.subscription_state == '5_renewed':
extra_payment_form_values['amount'] = 0
if manage_subscription:
extra_payment_form_values.update({
'subscription': order_sudo,
'allow_token_selection': True,
'allow_token_deletion': False,
'default_token_id': order_sudo.payment_token_id.id,
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
'assign_token_route': f'/my/subscriptions/assign_token/{sale_order_id}',
'landing_route': order_sudo.get_portal_url(),
})
if subscription_invoice_id:
invoice_id = self._cast_as_int(subscription_invoice_id)
access_token = kwargs.get('invoice_access_token')
try:
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)
if invoice_sudo.invoice_line_ids.subscription_id:
# Reroute the next steps of the payment flow to the portal view of the invoice.
# Add `is_subscription` variable in invoice information for differentiating subscriptions from regular SOs.
transaction_route = f'/my/subscriptions/invoice/{invoice_id}/transaction'
extra_payment_form_values.update({
'transaction_route_subscription': transaction_route,
'access_token': invoice_sudo.access_token,
'is_subscription': True,
})
return extra_payment_form_values
def _create_transaction(self, *args, **kwargs):
""" Override of payment to set subscriptions in pending states.
:param int sale_order_id: The sale order for which a payment id made, as a `sale.order` id
:param dict custom_create_values: Additional create values overwriting the default ones
:return: The result of the parent method
:rtype: recordset of `payment.transaction`
"""
tx_sudo = super()._create_transaction(
*args, **kwargs
)
if tx_sudo.operation != 'validation' and tx_sudo.state not in ['done', 'cancel', 'error']:
subscriptions = tx_sudo.sale_order_ids.filtered('is_subscription')
subscriptions.pending_transaction = True
return tx_sudo
@http.route('/my/subscriptions/<int:order_id>/transaction', type='jsonrpc', auth='public')
def subscription_transaction(
self, order_id, access_token, is_validation=False, **kwargs
):
""" Create a draft transaction and return its processing values.
:param int order_id: The subscription for which a transaction is made, as a `sale.order` id
:param str access_token: The access token of the subscription used to authenticate the partner
:param bool is_validation: Whether the operation is a validation
: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 subscription id or the access token is invalid
"""
order_sudo, redirection = self._get_subscription(access_token, order_id)
if redirection:
raise werkzeug.exceptions.NotFound()
logged_in = not request.env.user._is_public()
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_id
# Anticipate the next period by forcing invoice creation
subscription_anticipate = str2bool(kwargs.pop('subscription_anticipate', False))
self._validate_transaction_kwargs(kwargs)
kwargs.update(partner_id=partner_sudo.id)
if not is_validation: # Renewal transaction
invoice_to_pay = None
# When amount value is set and equal to None, fallback to zero
amount = kwargs.get('amount') or 0
if not subscription_anticipate:
for invoice in order_sudo.invoice_ids:
if (invoice.state == 'posted' and
invoice.move_type == 'out_invoice' and
invoice.payment_state not in ['paid', 'in_payment', 'reversed']):
invoice_to_pay = invoice
break
elif not invoice_to_pay and invoice.state == 'draft' and invoice.move_type == 'out_invoice':
invoice_to_pay = invoice
amount_to_invoice = invoice_to_pay.amount_total if invoice_to_pay else order_sudo.amount_to_invoice
amount = amount or amount_to_invoice
recurring_amount = sum(order_sudo.order_line.filtered(lambda l: l.recurring_invoice).mapped('price_total'))
is_zero = order_sudo.currency_id.is_zero(amount or recurring_amount)
tokenize = not is_zero and amount and recurring_amount and order_sudo.currency_id.compare_amounts(amount, recurring_amount) >= 0
kwargs.update({
'amount': amount,
'currency_id': order_sudo.currency_id.id,
'tokenization_requested': tokenize, # Renewal transactions are always tokenized
})
# Create the transaction.
tx_sudo = self._create_transaction(
custom_create_values={
'sale_order_ids': [Command.set([order_id])],
'invoice_ids': [Command.set([invoice_to_pay[:1].id])] if invoice_to_pay else [],
'subscription_action': 'assign_token' if tokenize else None,
},
is_validation=is_validation,
**kwargs
)
else: # Validation transaction
kwargs.update({
'amount': None, # The amount is computed when creating the transaction.
'currency_id': None, # The currency is computed when creating the transaction.
'reference_prefix': payment_utils.singularize_reference_prefix(
prefix='V' # Validation transactions use their own reference prefix
),
})
tx_sudo = self._create_transaction(
custom_create_values={
'sale_order_ids': [Command.set([order_id])],
'subscription_action': 'assign_token',
},
is_validation=is_validation,
**kwargs
)
return tx_sudo._get_processing_values()
@http.route('/my/subscriptions/invoice/<int:invoice_id>/transaction', type='jsonrpc', auth='public')
def subscription_transaction_from_invoice(
self, invoice_id, access_token, is_validation=False, **kwargs
):
# We should not do validation with this flow, return an empty dict instead.
if is_validation:
return {}
# Get the invoice record using the access token.
invoice_sudo = self._get_invoice_sudo(access_token, invoice_id)
# Validate the transaction's kwargs and then update it with invoice information.
self._validate_transaction_kwargs(kwargs)
logged_in = not request.env.user._is_public()
partner_sudo = request.env.user.partner_id if logged_in else invoice_sudo.partner_id
amount = kwargs.get('amount', 0) or invoice_sudo.amount_total
kwargs.update({
'partner_id': partner_sudo.id,
'amount': amount,
'currency_id': invoice_sudo.currency_id.id,
})
# Get subscriptions linked to the invoice lines and create the transaction linking to them.
subscriptions_sudo = invoice_sudo.invoice_line_ids.subscription_id
# Note: tokenize will always be True in this flow (unautomated goes via account_payment)
tokenize = kwargs.get('tokenization_requested') or kwargs.get('flow') == 'token'
tx_sudo = self._create_transaction(
custom_create_values={
'sale_order_ids': [Command.set(subscriptions_sudo.ids)],
'invoice_ids': [Command.set(invoice_sudo.ids)],
'subscription_action': 'assign_token' if tokenize else 'manual_send_mail',
},
is_validation=False,
**kwargs
)
return tx_sudo._get_processing_values()
@http.route('/my/subscriptions/assign_token/<int:order_id>', type='jsonrpc', auth='user')
def subscription_assign_token(self, order_id, token_id, access_token=None):
""" Assign a token to a subscription.
:param int order_id: The subscription to which the token must be assigned, as a
`sale.order` id
:param int token_id: The token to assign, as a `payment.token` id
:param str access_token: the order portal access token
:return: None
"""
order_sudo, redirection = self._get_subscription(access_token, order_id)
partner_id = request.env.user.partner_id
if redirection:
return redirection
token_sudo = request.env['payment.token'].sudo().search([
('id', '=', token_id),
('partner_id', 'child_of', partner_id.commercial_partner_id.id),
# Bypass active_test context to make sure no archived tokens are used.
('active', '=', True),
])
if not token_sudo:
# Archived token are removed from existing subscriptions
# and shouldn't be re-assigned through this route.
raise werkzeug.exceptions.NotFound()
max_amount = token_sudo.provider_id.maximum_amount
if max_amount > 0 and order_sudo.currency_id.compare_amounts(order_sudo.amount_total, max_amount) == 1:
raise UserError(_(
"The payment method you selected can only pay amounts up to %s. Please create or select another one.",
token_sudo.provider_id.main_currency_id.format(max_amount),
))
order_sudo.payment_token_id = token_sudo
@http.route()
def payment_method(self, sale_order_id=None, **kwargs):
""" Override to cast sale_order_id passed in via token_management_url to an int. """
return super().payment_method(sale_order_id=self._cast_as_int(sale_order_id), **kwargs)
def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
""" Override of account to allow save the token on the sale.order when the invoice
is related to a subscription. It allows to save the token automatically on the sale.order if
the customer chose to pay the invoice with that option.
"""
subscription_id = invoice.line_ids.subscription_id
if invoice and subscription_id:
# The function "_get_extra_payment_form_values" needs the invoice inside the kwargs to work.
kwargs.update({
'subscription_invoice_id': invoice.id,
'invoice_access_token': access_token,
**kwargs
})
return super()._invoice_get_page_view_values(invoice, access_token, **kwargs)
class SalePortal(sale_portal.CustomerPortal):
def _get_payment_values(self, order_sudo, is_subscription=False, subscription_anticipate=False, **kwargs):
""" Override of `sale` to specify whether the sales order is a subscription.
:param sale.order order_sudo: The sales order being paid.
:param bool is_subscription: Whether the order is a subscription.
:param dict kwargs: Locally unused keywords arguments.
:return: The payment-specific values.
:rtype: dict
"""
is_subscription = is_subscription \
or order_sudo.is_subscription \
or order_sudo.subscription_id.is_subscription
return {
**super()._get_payment_values(order_sudo, is_subscription=is_subscription,
subscription_anticipate=subscription_anticipate,
**kwargs),
'is_subscription': is_subscription,
}