Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions addons/event/tests/test_event_slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,19 @@ def test_seats_slots_notickets(self):
new.with_user(self.user_eventmanager).unlink()

# check ``_verify_seats_availability`` itself
for check_input, should_crash in [
for check_input, should_crash, problematic_name in [
# ok for event max seats for both slots
(((first_slot, False, 2), (second_slot, False, 4)), False),
(((first_slot, False, 2), (second_slot, False, 4)), False, None),
# not enough seats on first slot
(((first_slot, False, 3),), True),
(((first_slot, False, 3),), True, first_slot.display_name),
# not enough seats on second slot
(((second_slot, False, 5),), True),
(((second_slot, False, 5),), True, second_slot.display_name),
]:
with self.subTest(check_input=check_input, should_crash=should_crash):
with self.subTest(check_input=check_input, should_crash=should_crash, problematic_name=problematic_name):
if should_crash:
with self.assertRaises(exceptions.ValidationError):
# Error message should reference the slot name.
# See `sale.order._is_cart_ready_for_checkout` for a why.
with self.assertRaisesRegex(exceptions.ValidationError, problematic_name):
test_event._verify_seats_availability(check_input)
else:
test_event._verify_seats_availability(check_input)
Expand Down
17 changes: 13 additions & 4 deletions addons/payment_stripe/static/src/interactions/express_checkout.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* global Stripe */

import { patch } from '@web/core/utils/patch';
import { ExpressCheckout } from '@payment/interactions/express_checkout';
import { StripeOptions } from '@payment_stripe/js/stripe_options';
import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';
import { _t } from '@web/core/l10n/translation';
import { rpc } from '@web/core/network/rpc';
import { patch } from '@web/core/utils/patch';
import { redirect } from '@web/core/utils/urls';
import { ExpressCheckout } from '@payment/interactions/express_checkout';
import { StripeOptions } from '@payment_stripe/js/stripe_options';

patch(ExpressCheckout.prototype, {
/**
Expand Down Expand Up @@ -136,7 +137,15 @@ patch(ExpressCheckout.prototype, {
const { client_secret } = await this.waitFor(rpc(
this.paymentContext['transactionRoute'],
this._prepareTransactionRouteParams(providerData.providerId),
));
).catch(error => {
ev.complete('fail');
this.call('dialog', 'add', ConfirmationDialog, {
title: _t("Payment failed"),
body: error.data.message || "",
});
return {};
}));
if (!client_secret) return;
// Confirm the PaymentIntent without handling eventual next actions (e.g. 3DS).
const { paymentIntent, error: confirmError } = await this.waitFor(
stripeJS.confirmCardPayment(
Expand Down
1 change: 0 additions & 1 deletion addons/website_event_sale/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-

from . import main
from . import payment
from . import sale
32 changes: 0 additions & 32 deletions addons/website_event_sale/controllers/payment.py

This file was deleted.

35 changes: 34 additions & 1 deletion addons/website_event_sale/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import _, models
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError


class SaleOrder(models.Model):
Expand Down Expand Up @@ -125,3 +125,36 @@ def _filter_can_send_abandoned_cart_mail(self):
return super()._filter_can_send_abandoned_cart_mail().filtered(
lambda so: all(ticket.sale_available for ticket in so.order_line.event_ticket_id),
)

def _is_cart_ready_for_checkout(self):
"""Override of `website_sale` to check if the user is trying to order a ticket that is no
longer available."""
ready = super()._is_cart_ready_for_checkout()
if not (ready and self.order_line.event_id):
return ready

# Check that there are enough seats available per ticket.
registration_domain = [
('sale_order_id', '=', self.id),
('event_ticket_id', '!=', False),
('state', '!=', 'cancel'),
]
registrations_per_event = self.env['event.registration'].sudo()._read_group(
registration_domain, ['event_id'], ['id:recordset'],
)
errors = []
for event, registrations in registrations_per_event:
count_per_slot_ticket = self.env['event.registration'].sudo()._read_group(
[('id', 'in', registrations.ids)],
['event_slot_id', 'event_ticket_id'],
['__count'],
)
try:
event._verify_seats_availability(count_per_slot_ticket)
except ValidationError as e:
errors.append(str(e))

if errors:
# The error messages already inform the customer about which ticket is unavailable.
self.shop_warning = "\n".join(errors)
return not errors
1 change: 1 addition & 0 deletions addons/website_event_sale/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import common
from . import test_event_checkout
from . import test_frontend_buy_tickets
from . import test_website_event_sale_cart
from . import test_website_event_sale_pricelist
Expand Down
55 changes: 55 additions & 0 deletions addons/website_event_sale/tests/test_event_checkout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo.fields import Command
from odoo.tests import HttpCase, tagged

from odoo.addons.website_event_sale.tests.common import TestWebsiteEventSaleCommon
from odoo.addons.website_sale.controllers.main import WebsiteSale as CheckoutController
from odoo.addons.website_sale.tests.common import MockRequest


@tagged('post_install', '-at_install')
class TestEventCheckout(TestWebsiteEventSaleCommon, HttpCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.CheckoutController = CheckoutController()

def test_checkout_impossible_if_tickets_are_expired(self):
self.ticket.write({
'seats_max': 1,
'seats_limited': True,
})

so1 = self._create_so(order_line=[Command.create({
'product_id': self.ticket.product_id.id,
'event_id': self.event.id,
'event_ticket_id': self.ticket.id,
})])
so2 = self._create_so(order_line=[Command.create({
'product_id': self.ticket.product_id.id,
'event_id': self.event.id,
'event_ticket_id': self.ticket.id,
})])
self.env['event.registration'].create([
{
'state': 'draft',
'sale_order_id': so1.id,
'partner_id': so1.partner_id.id,
'event_id': self.event.id,
'event_ticket_id': self.ticket.id,
},
])

so2.action_confirm()
self.assertEqual(self.event.seats_available, 0)
self.assertEqual(self.event.seats_taken, 1)

website = self.website.with_user(self.public_user)
with MockRequest(website.env, website=website, path='/shop/checkout', sale_order_id=so1.id):
response = self.CheckoutController.shop_checkout()

# SO2 was too quick and registered before SO1 => redirected to change tickets
self.assertEqual(response.status_code, 303, 'SEE OTHER')
self.assertURLEqual(response.location, '/shop/cart')
90 changes: 38 additions & 52 deletions addons/website_sale/controllers/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def cart(self, id=None, access_token=None, revive_method='', **post):

values.update({
'website_sale_order': order_sudo,
'order': order_sudo,
'shop_warnings': order_sudo._pop_shop_warnings(),
'date': fields.Date.today(),
'suggested_products': [],
})
Expand Down Expand Up @@ -223,31 +225,7 @@ def add_to_cart(
)
def quick_add(self, product_template_id, product_id, quantity=1.0, **kwargs):
values = self.add_to_cart(product_template_id, product_id, quantity=quantity, **kwargs)

IrUiView = request.env['ir.ui.view']
order_sudo = request.cart
values['website_sale.cart_lines'] = IrUiView._render_template(
'website_sale.cart_lines', {
'website_sale_order': order_sudo,
'date': fields.Date.today(),
'suggested_products': order_sudo._cart_accessories(),
}
)
values['website_sale.shorter_cart_summary'] = IrUiView._render_template(
'website_sale.shorter_cart_summary', {
'website_sale_order': order_sudo,
'show_shorter_cart_summary': True,
**self._get_express_shop_payment_values(order_sudo),
**request.website._get_checkout_step_values(),
}
)
values['website_sale.quick_reorder_history'] = IrUiView._render_template(
'website_sale.quick_reorder_history', {
'website_sale_order': order_sudo,
**self._prepare_order_history(),
}
)
values['cart_ready'] = order_sudo._is_cart_ready()
values.update(self._get_cart_update_values(request.cart))
return values

def _get_express_shop_payment_values(self, order, **kwargs):
Expand Down Expand Up @@ -298,7 +276,6 @@ def update_cart(self, line_id, quantity, product_id=None, **kwargs):
"""
order_sudo = request.cart
quantity = int(quantity) # Do not allow float values in ecommerce by default
IrUiView = request.env['ir.ui.view']

# This method must be only called from the cart page BUT in some advanced logic
# eg. website_sale_loyalty, a cart line could be a temporary record without id.
Expand All @@ -309,34 +286,43 @@ def update_cart(self, line_id, quantity, product_id=None, **kwargs):
)[:1].id

values = order_sudo._cart_update_line_quantity(line_id, quantity, **kwargs)
values.update(self._get_cart_update_values(order_sudo))
return values

def _get_cart_update_values(self, order_sudo):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name seems kinda confusing to me, sounds like the values you provide to a method call, not the values you get as feedback from the request.

"""Construct the values needed to update the UI after a cart update.

:param sale.order order_sudo: The current cart order.
:rtype: dict
"""
IrUiView = request.env['ir.ui.view']

values['cart_quantity'] = order_sudo.cart_quantity
values['cart_ready'] = order_sudo._is_cart_ready()
values['amount'] = order_sudo.amount_total
values['minor_amount'] = (
order_sudo and payment_utils.to_minor_currency_units(
return {
'cart_quantity': order_sudo.cart_quantity,
'cart_ready': order_sudo._is_cart_ready_for_checkout(),
'amount': order_sudo.amount_total,
'minor_amount': payment_utils.to_minor_currency_units(
order_sudo.amount_total, order_sudo.currency_id
)
) or 0.0
values['website_sale.cart_lines'] = IrUiView._render_template(
'website_sale.cart_lines', {
'website_sale_order': order_sudo,
'date': fields.Date.today(),
'suggested_products': order_sudo._cart_accessories()
}
)
values['website_sale.total'] = IrUiView._render_template(
'website_sale.total', {
'website_sale_order': order_sudo,
}
)
values['website_sale.quick_reorder_history'] = IrUiView._render_template(
'website_sale.quick_reorder_history', {
'website_sale_order': order_sudo,
**self._prepare_order_history(),
}
)
return values
),
'website_sale.cart_lines': IrUiView._render_template(
'website_sale.cart_lines', {
'website_sale_order': order_sudo,
'date': fields.Date.today(),
'suggested_products': order_sudo._cart_accessories(),
'order': order_sudo,
'shop_warnings': order_sudo._pop_shop_warnings(),
}
),
'website_sale.total': IrUiView._render_template(
'website_sale.total', {'website_sale_order': order_sudo}
),
'website_sale.quick_reorder_history': IrUiView._render_template(
'website_sale.quick_reorder_history', {
'website_sale_order': order_sudo,
**self._prepare_order_history(),
}
),
}

def _prepare_order_history(self):
"""Prepare the order history of the current user.
Expand Down
Loading