-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
views.py
818 lines (678 loc) · 33.3 KB
/
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
import logging
from django.http import Http404, HttpResponseRedirect, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import login
from django.db.models import get_model
from django.utils.translation import ugettext as _
from django.views.generic import DetailView, TemplateView, FormView, \
DeleteView, UpdateView, CreateView
from oscar.apps.shipping.methods import Free
from oscar.core.loading import get_class, get_classes
ShippingAddressForm, GatewayForm = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
pre_payment, post_payment = get_classes('checkout.signals', ['pre_payment', 'post_payment'])
OrderNumberGenerator, OrderCreator = get_classes('order.utils', ['OrderNumberGenerator', 'OrderCreator'])
UserAddressForm = get_class('address.forms', 'UserAddressForm')
Repository = get_class('shipping.repository', 'Repository')
AccountAuthView = get_class('customer.views', 'AccountAuthView')
Dispatcher = get_class('customer.utils', 'Dispatcher')
RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
Order = get_model('order', 'Order')
ShippingAddress = get_model('order', 'ShippingAddress')
CommunicationEvent = get_model('order', 'CommunicationEvent')
PaymentEventType = get_model('order', 'PaymentEventType')
PaymentEvent = get_model('order', 'PaymentEvent')
UserAddress = get_model('address', 'UserAddress')
Basket = get_model('basket', 'Basket')
Email = get_model('customer', 'Email')
CommunicationEventType = get_model('customer', 'CommunicationEventType')
# Standard logger for checkout events
logger = logging.getLogger('oscar.checkout')
class CheckoutSessionMixin(object):
"""
Mixin to provide common functionality shared between checkout views.
"""
def dispatch(self, request, *args, **kwargs):
self.checkout_session = CheckoutSessionData(request)
return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
def get_shipping_address(self):
"""
Return the current shipping address for this checkout session.
This could either be a ShippingAddress model which has been
pre-populated (not saved), or a UserAddress model which will
need converting into a ShippingAddress model at submission
"""
addr_data = self.checkout_session.new_shipping_address_fields()
if addr_data:
# Load address data into a blank address model
return ShippingAddress(**addr_data)
addr_id = self.checkout_session.user_address_id()
if addr_id:
try:
return UserAddress._default_manager.get(pk=addr_id)
except UserAddress.DoesNotExist:
# This can happen if you reset all your tables and you still have
# session data that refers to addresses that no longer exist
pass
return None
def get_shipping_method(self, basket=None):
method = self.checkout_session.shipping_method()
if method:
if not basket:
basket = self.request.basket
method.set_basket(basket)
else:
# We default to using free shipping
method = Free()
return method
def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
"""
Returns the total for the order with and without tax (as a tuple)
"""
calc = OrderTotalCalculator(self.request)
if not basket:
basket = self.request.basket
if not shipping_method:
shipping_method = self.get_shipping_method(basket)
total_incl_tax = calc.order_total_incl_tax(basket, shipping_method, **kwargs)
total_excl_tax = calc.order_total_excl_tax(basket, shipping_method, **kwargs)
return total_incl_tax, total_excl_tax
def get_context_data(self, **kwargs):
"""
Assign common template variables to the context.
"""
ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
ctx['shipping_address'] = self.get_shipping_address()
method = self.get_shipping_method()
if method:
ctx['shipping_method'] = method
ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
return ctx
class IndexView(CheckoutSessionMixin, FormView):
"""
First page of the checkout. We prompt user to either sign in, or
to proceed as a guest (where we still collect their email address).
"""
template_name = 'checkout/gateway.html'
form_class = GatewayForm
def get(self, request, *args, **kwargs):
if request.user.is_authenticated():
return self.get_success_response()
return super(IndexView, self).get(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(IndexView, self).get_form_kwargs()
email = self.checkout_session.get_guest_email()
if email:
kwargs['initial'] = {
'username': email,
'options': 'new'
}
return kwargs
def form_valid(self, form):
if form.is_guest_checkout():
email = form.cleaned_data['username']
self.checkout_session.set_guest_email(email)
else:
user = form.get_user()
login(self.request, user)
return self.get_success_response()
def get_success_response(self):
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('checkout:shipping-address')
# ================
# SHIPPING ADDRESS
# ================
class ShippingAddressView(CheckoutSessionMixin, FormView):
"""
Determine the shipping address for the order.
The default behaviour is to display a list of addresses from the users's
address book, from which the user can choose one to be their shipping address.
They can add/edit/delete these USER addresses. This address will be
automatically converted into a SHIPPING address when the user checks out.
Alternatively, the user can enter a SHIPPING address directly which will be
saved in the session and saved as a model when the order is sucessfully submitted.
"""
template_name = 'checkout/shipping_address.html'
form_class = ShippingAddressForm
def get(self, request, *args, **kwargs):
# Check that guests have entered an email address
if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
messages.error(request, _("Please either sign in or enter your email address"))
return HttpResponseRedirect(reverse('checkout:index'))
return super(ShippingAddressView, self).get(request, *args, **kwargs)
def get_initial(self):
return self.checkout_session.new_shipping_address_fields()
def get_context_data(self, **kwargs):
kwargs = super(ShippingAddressView, self).get_context_data(**kwargs)
if self.request.user.is_authenticated():
# Look up address book data
kwargs['addresses'] = self.get_available_addresses()
return kwargs
def get_available_addresses(self):
return UserAddress._default_manager.filter(user=self.request.user).order_by('-is_default_for_shipping')
def post(self, request, *args, **kwargs):
# Check if a shipping address was selected directly (eg no form was filled in)
if self.request.user.is_authenticated and 'address_id' in self.request.POST:
address = UserAddress._default_manager.get(pk=self.request.POST['address_id'])
if 'action' in self.request.POST and self.request.POST['action'] == 'ship_to':
# User has selected a previous address to ship to
self.checkout_session.ship_to_user_address(address)
return HttpResponseRedirect(self.get_success_url())
elif 'action' in self.request.POST and self.request.POST['action'] == 'delete':
address.delete()
messages.info(self.request, "Address deleted from your address book")
return HttpResponseRedirect(reverse('checkout:shipping-method'))
else:
return HttpResponseBadRequest()
else:
return super(ShippingAddressView, self).post(request, *args, **kwargs)
def form_valid(self, form):
# Store the address details in the session and redirect to next step
self.checkout_session.ship_to_new_address(form.clean())
return super(ShippingAddressView, self).form_valid(form)
def get_success_url(self):
return reverse('checkout:shipping-method')
class UserAddressCreateView(CheckoutSessionMixin, CreateView):
"""
Add a USER address to the user's addressbook.
This is not the same as creating a SHIPPING Address, although if used for the order,
it will be converted into a shipping address at submission-time.
"""
template_name = 'checkout/user_address_form.html'
form_class = UserAddressForm
def get_context_data(self, **kwargs):
kwargs = super(UserAddressCreateView, self).get_context_data(**kwargs)
kwargs['form_url'] = reverse('checkout:user-address-create')
return kwargs
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()
return self.get_success_response()
def get_success_response(self):
messages.info(self.request, _("Address saved"))
# We redirect back to the shipping address page
return HttpResponseRedirect(reverse('checkout:shipping-address'))
class UserAddressUpdateView(CheckoutSessionMixin, UpdateView):
"""
Update a user address
"""
template_name = 'checkout/user_address_form.html'
form_class = UserAddressForm
def get_queryset(self):
return UserAddress._default_manager.filter(user=self.request.user)
def get_context_data(self, **kwargs):
kwargs = super(UserAddressUpdateView, self).get_context_data(**kwargs)
kwargs['form_url'] = reverse('checkout:user-address-update', args=(str(kwargs['object'].id),))
return kwargs
def get_success_url(self):
messages.info(self.request, _("Address saved"))
return reverse('checkout:shipping-address')
class UserAddressDeleteView(CheckoutSessionMixin, DeleteView):
"""
Delete an address from a user's addressbook.
"""
template_name = 'checkout/user_address_delete.html'
def get_queryset(self):
return UserAddress._default_manager.filter(user=self.request.user)
def get_success_url(self):
messages.info(self.request, _("Address deleted"))
return reverse('checkout:shipping-address')
# ===============
# Shipping method
# ===============
class ShippingMethodView(CheckoutSessionMixin, TemplateView):
"""
View for allowing a user to choose a shipping method.
Shipping methods are largely domain-specific and so this view
will commonly need to be subclassed and customised.
The default behaviour is to load all the available shipping methods
using the shipping Repository. If there is only 1, then it is
automatically selected. Otherwise, a page is rendered where
the user can choose the appropriate one.
"""
template_name = 'checkout/shipping_methods.html';
def get(self, request, *args, **kwargs):
# Check that shipping address has been completed
if not self.checkout_session.is_shipping_address_set():
messages.error(request, _("Please choose a shipping address"))
return HttpResponseRedirect(reverse('checkout:shipping-address'))
# Save shipping methods as instance var as we need them both here
# and when setting the context vars.
self._methods = self.get_available_shipping_methods()
if len(self._methods) == 1:
# Only one shipping method - set this and redirect onto the next step
self.checkout_session.use_shipping_method(self._methods[0].code)
return self.get_success_response()
return super(ShippingMethodView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs = super(ShippingMethodView, self).get_context_data(**kwargs)
kwargs['methods'] = self._methods
return kwargs
def get_available_shipping_methods(self):
"""
Returns all applicable shipping method objects
for a given basket.
"""
# Shipping methods can depend on the user, the contents of the basket
# and the shipping address. I haven't come across a scenario that doesn't
# fit this system.
return Repository().get_shipping_methods(self.request.user, self.request.basket,
self.get_shipping_address())
def post(self, request, *args, **kwargs):
# Need to check that this code is valid for this user
method_code = request.POST.get('method_code', None)
is_valid = False
for method in self.get_available_shipping_methods():
if method.code == method_code:
is_valid = True
if not is_valid:
messages.error(request, _("Your submitted shipping method is not permitted"))
return HttpResponseRedirect(reverse('checkout:shipping-method'))
# Save the code for the chosen shipping method in the session
# and continue to the next step.
self.checkout_session.use_shipping_method(method_code)
return self.get_success_response()
def get_success_response(self):
return HttpResponseRedirect(reverse('checkout:payment-method'))
# ==============
# Payment method
# ==============
class PaymentMethodView(CheckoutSessionMixin, TemplateView):
"""
View for a user to choose which payment method(s) they want to use.
This would include setting allocations if payment is to be split
between multiple sources.
"""
def get(self, request, *args, **kwargs):
# Check that shipping address has been completed
if not self.checkout_session.is_shipping_address_set():
messages.error(request, _("Please choose a shipping address"))
return HttpResponseRedirect(reverse('checkout:shipping-address'))
# Check that shipping method has been set
if not self.checkout_session.is_shipping_method_set():
messages.error(request, _("Please choose a shipping method"))
return HttpResponseRedirect(reverse('checkout:shipping-method'))
return self.get_success_response()
def get_success_response(self):
return HttpResponseRedirect(reverse('checkout:payment-details'))
# ================
# Order submission
# ================
class OrderPlacementMixin(CheckoutSessionMixin):
"""
Mixin which provides functionality for placing orders.
"""
# Any payment sources should be added to this list as part of the
# _handle_payment method. If the order is placed successfully, then
# they will be persisted.
_payment_sources = None
_payment_events = None
# Default code for the email to send after successful checkout
communication_type_code = 'ORDER_PLACED'
def handle_order_placement(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
"""
Write out the order models and return the appropriate HTTP response
We deliberately pass the basket in here as the one tied to the request
isn't necessarily the correct one to use in placing the order. This can
happen when a basket gets frozen.
"""
order = self.place_order(order_number, basket, total_incl_tax, total_excl_tax, **kwargs)
basket.set_as_submitted()
return self.handle_successful_order(order)
def add_payment_source(self, source):
if self._payment_sources is None:
self._payment_sources = []
self._payment_sources.append(source)
def add_payment_event(self, event_type_name, amount):
event_type, n = PaymentEventType.objects.get_or_create(name=event_type_name)
if self._payment_events is None:
self._payment_events = []
event = PaymentEvent(event_type=event_type, amount=amount)
self._payment_events.append(event)
def handle_successful_order(self, order):
"""
Handle the various steps required after an order has been successfully placed.
Override this view if you want to perform custom actions when an
order is submitted.
"""
# Send confirmation message (normally an email)
self.send_confirmation_message(order)
# Flush all session data
self.checkout_session.flush()
# Save order id in session so thank-you page can load it
self.request.session['checkout_order_id'] = order.id
return HttpResponseRedirect(reverse('checkout:thank-you'))
def place_order(self, order_number, basket, total_incl_tax, total_excl_tax, **kwargs):
"""
Writes the order out to the DB including the payment models
"""
shipping_address = self.create_shipping_address()
shipping_method = self.get_shipping_method(basket)
billing_address = self.create_billing_address(shipping_address)
if 'status' not in kwargs:
status = self.get_initial_order_status(basket)
else:
status = kwargs.pop('status')
# Set guest email address for anon checkout. Some libraries (eg
# PayPal) will pass this explicitly so we take care not to clobber.
if not self.request.user.is_authenticated() and 'guest_email' not in kwargs:
kwargs['guest_email'] = self.checkout_session.get_guest_email()
order = OrderCreator().place_order(basket=basket,
total_incl_tax=total_incl_tax,
total_excl_tax=total_excl_tax,
user=self.request.user,
shipping_method=shipping_method,
shipping_address=shipping_address,
billing_address=billing_address,
order_number=order_number,
status=status,
**kwargs)
self.save_payment_details(order)
return order
def create_shipping_address(self):
"""
Create and returns the shipping address for the current order.
If the shipping address was entered manually, then we simply
write out a ShippingAddress model with the appropriate form data. If
the user is authenticated, then we create a UserAddress from this data
too so it can be re-used in the future.
If the shipping address was selected from the user's address book,
then we convert the UserAddress to a ShippingAddress.
"""
addr_data = self.checkout_session.new_shipping_address_fields()
addr_id = self.checkout_session.user_address_id()
if addr_data:
addr = self.create_shipping_address_from_form_fields(addr_data)
self.create_user_address(addr_data)
elif addr_id:
addr = self.create_shipping_address_from_user_address(addr_id)
else:
raise AttributeError("No shipping address data found")
return addr
def create_shipping_address_from_form_fields(self, addr_data):
"""Creates a shipping address model from the saved form fields"""
shipping_addr = ShippingAddress(**addr_data)
shipping_addr.save()
return shipping_addr
def create_user_address(self, addr_data):
"""
For signed-in users, we create a user address model which will go
into their address book.
"""
if self.request.user.is_authenticated():
addr_data['user_id'] = self.request.user.id
user_addr = UserAddress(**addr_data)
# Check that this address isn't already in the db as we don't want
# to fill up the customer address book with duplicate addresses
try:
UserAddress._default_manager.get(hash=user_addr.generate_hash())
except ObjectDoesNotExist:
user_addr.save()
def create_shipping_address_from_user_address(self, addr_id):
"""Creates a shipping address from a user address"""
address = UserAddress._default_manager.get(pk=addr_id)
# Increment the number of orders to help determine popularity of orders
address.num_orders += 1
address.save()
shipping_addr = ShippingAddress()
address.populate_alternative_model(shipping_addr)
shipping_addr.save()
return shipping_addr
def create_billing_address(self, shipping_address=None):
"""
Saves any relevant billing data (eg a billing address).
"""
return None
def save_payment_details(self, order):
"""
Saves all payment-related details. This could include a billing
address, payment sources and any order payment events.
"""
self.save_payment_events(order)
self.save_payment_sources(order)
def save_payment_events(self, order):
"""
Saves any relevant payment events for this order
"""
if not self._payment_events:
return
for event in self._payment_events:
event.order = order
event.save()
def save_payment_sources(self, order):
"""
Saves any payment sources used in this order.
When the payment sources are created, the order model does not exist and
so they need to have it set before saving.
"""
if not self._payment_sources:
return
for source in self._payment_sources:
source.order = order
source.save()
def get_initial_order_status(self, basket):
return None
def get_submitted_basket(self):
basket_id = self.checkout_session.get_submitted_basket_id()
return Basket._default_manager.get(pk=basket_id)
def restore_frozen_basket(self):
"""
Restores a frozen basket as the sole OPEN basket. Note that this also merges
in any new products that have been added to a basket that has been created while payment.
"""
try:
fzn_basket = self.get_submitted_basket()
except Basket.DoesNotExist:
# Strange place. The previous basket stored in the session does
# not exist.
pass
else:
fzn_basket.thaw()
if self.request.basket.id != fzn_basket.id:
fzn_basket.merge(self.request.basket)
self.request.basket = fzn_basket
def send_confirmation_message(self, order, **kwargs):
code = self.communication_type_code
ctx = {'order': order,
'lines': order.lines.all(),}
try:
event_type = CommunicationEventType.objects.get(code=code)
except CommunicationEventType.DoesNotExist:
# No event in database, attempt to find templates for this type
messages = CommunicationEventType.objects.get_and_render(code, ctx)
event_type = None
else:
# Create order event
CommunicationEvent._default_manager.create(order=order, event_type=event_type)
messages = event_type.get_messages(ctx)
if messages and messages['body']:
logger.info("Order #%s - sending %s messages", order.number, code)
dispatcher = Dispatcher(logger)
dispatcher.dispatch_order_messages(order, messages, event_type, **kwargs)
else:
logger.warning("Order #%s - no %s communication event type", order.number, code)
class PaymentDetailsView(OrderPlacementMixin, TemplateView):
"""
For taking the details of payment and creating the order
The class is deliberately split into fine-grained methods, responsible for only one
thing. This is to make it easier to subclass and override just one component of
functionality.
Almost all projects will need to subclass and customise this class.
"""
template_name = 'checkout/payment_details.html'
template_name_preview = 'checkout/preview.html'
preview = False
def get_template_names(self):
return [self.template_name_preview] if self.preview else [self.template_name]
def get_error_response(self):
# Check that shipping address has been completed
if not self.checkout_session.is_shipping_address_set():
messages.error(self.request, _("Please choose a shipping address"))
return HttpResponseRedirect(reverse('checkout:shipping-address'))
# Check that shipping method has been set
if not self.checkout_session.is_shipping_method_set():
messages.error(self.request, _("Please choose a shipping method"))
return HttpResponseRedirect(reverse('checkout:shipping-method'))
def get(self, request, *args, **kwargs):
error_response = self.get_error_response()
if error_response:
return error_response
return super(PaymentDetailsView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
This method is designed to be overridden by subclasses which will
validate the forms from the payment details page. If the forms are valid
then the method can call submit()
"""
error_response = self.get_error_response()
if error_response:
return error_response
if self.preview:
return self.render_preview(request, *args, **kwargs)
return self.submit(request.basket, **kwargs)
def render_preview(self, request, **kwargs):
"""
Show a preview of the order.
If sensitive data was submitted on the payment details page, you will
need to pass it back to the view here so it can be stored in hidden form
inputs. This avoids ever writing the sensitive data to disk.
"""
ctx = self.get_context_data()
ctx.update(kwargs)
return self.render_to_response(ctx)
def can_basket_be_submitted(self, basket):
for line in basket.lines.all():
is_permitted, reason = line.product.is_purchase_permitted(self.request.user, line.quantity)
if not is_permitted:
return False, reason, reverse('basket:summary')
return True, None, None
def get_default_billing_address(self):
"""
Return default billing address for user
This is useful when the payment details view includes a billing address
form - you can use this helper method to prepopulate the form.
Note, this isn't used in core oscar as there is no billing address form
by default.
"""
if not self.request.user.is_authenticated():
return None
try:
return self.request.user.addresses.get(is_default_for_billing=True)
except UserAddress.DoesNotExist:
return None
def submit(self, basket, **kwargs):
"""
Submit a basket for order placement.
The process runs as follows:
* Generate an order number
* Freeze the basket so it cannot be modified any more.
* Attempt to take payment for the order
- If payment is successful, place the order
- If a redirect is required (eg PayPal, 3DSecure), redirect
- If payment is unsuccessful, show an appropriate error message
"""
# Next, check that basket isn't empty
if basket.is_empty:
messages.error(self.request, _("This order cannot be submitted as the basket is empty"))
url = self.request.META.get('HTTP_REFERER', reverse('checkout:shipping-address'))
return HttpResponseRedirect(url)
# Domain-specific checks on the basket
is_valid, reason, url = self.can_basket_be_submitted(basket)
if not is_valid:
messages.error(self.request, reason)
return HttpResponseRedirect(url)
# We generate the order number first as this will be used
# in payment requests (ie before the order model has been
# created). We also save it in the session for multi-stage
# checkouts (eg where we redirect to a 3rd party site and place
# the order on a different request).
order_number = self.generate_order_number(basket)
logger.info("Order #%s: beginning submission process for basket %d", order_number, basket.id)
self.freeze_basket(basket)
self.checkout_session.set_submitted_basket(basket)
# Handle payment. Any payment problems should be handled by the
# handle_payment method raise an exception, which should be caught
# within handle_POST and the appropriate forms redisplayed.
try:
pre_payment.send_robust(sender=self, view=self)
total_incl_tax, total_excl_tax = self.get_order_totals(basket)
self.handle_payment(order_number, total_incl_tax, **kwargs)
post_payment.send_robust(sender=self, view=self)
except RedirectRequired, e:
# Redirect required (eg PayPal, 3DS)
logger.info("Order #%s: redirecting to %s", order_number, e.url)
return HttpResponseRedirect(e.url)
except UnableToTakePayment, e:
# Something went wrong with payment, need to show
# error to the user. This type of exception is supposed
# to set a friendly error message.
msg = unicode(e)
logger.warning("Order #%s: unable to take payment (%s) - restoring basket", order_number, msg)
self.restore_frozen_basket()
return self.render_to_response(self.get_context_data(error=msg))
except PaymentError, e:
# Something went wrong which wasn't anticipated.
msg = unicode(e)
logger.error("Order #%s: payment error (%s)", order_number, msg)
self.restore_frozen_basket()
return self.render_to_response(self.get_context_data(error="A problem occurred processing payment."))
# If all is ok with payment, try and place order
logger.info("Order #%s: payment successful, placing order", order_number)
try:
return self.handle_order_placement(order_number, basket, total_incl_tax, total_excl_tax, **kwargs)
except UnableToPlaceOrder, e:
logger.warning("Order #%s: unable to place order - %s",
order_number, e)
msg = unicode(e)
self.restore_frozen_basket()
return self.render_to_response(self.get_context_data(error=msg))
def generate_order_number(self, basket):
generator = OrderNumberGenerator()
order_number = generator.order_number(basket)
self.checkout_session.set_order_number(order_number)
return order_number
def freeze_basket(self, basket):
# We freeze the basket to prevent it being modified once the payment
# process has started. If your payment fails, then the basket will
# need to be "unfrozen". We also store the basket ID in the session
# so the it can be retrieved by multistage checkout processes.
basket.freeze()
def handle_payment(self, order_number, total, **kwargs):
"""
Handle any payment processing.
This method is designed to be overridden within your project. The
default is to do nothing.
"""
pass
# =========
# Thank you
# =========
class ThankYouView(DetailView):
"""
Displays the 'thank you' page which summarises the order just submitted.
"""
template_name = 'checkout/thank_you.html'
context_object_name = 'order'
def get_object(self):
# We allow superusers to force an order thankyou page for testing
order = None
if self.request.user.is_superuser:
if 'order_number' in self.request.GET:
order = Order._default_manager.get(number=self.request.GET['order_number'])
elif 'order_id' in self.request.GET:
order = Order._default_manager.get(id=self.request.GET['orderid'])
if not order:
if 'checkout_order_id' in self.request.session:
order = Order._default_manager.get(pk=self.request.session['checkout_order_id'])
else:
raise Http404(_("No order found"))
return order