You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

views.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import csv
  2. from decimal import Decimal as D, InvalidOperation
  3. from django.contrib import messages
  4. from django.core.urlresolvers import reverse
  5. from django.core.exceptions import ObjectDoesNotExist
  6. from django.db.models.loading import get_model
  7. from django.db.models import Sum, Count, fields, Q
  8. from django.http import HttpResponse, HttpResponseRedirect, Http404
  9. from django.shortcuts import get_object_or_404
  10. from django.template.defaultfilters import date as format_date
  11. from django.utils.datastructures import SortedDict
  12. from django.views.generic import TemplateView, ListView, DetailView, UpdateView, FormView
  13. from django.contrib import messages
  14. from oscar.core.loading import get_class
  15. from oscar.apps.dashboard.orders import forms
  16. from oscar.apps.dashboard.views import BulkEditMixin
  17. from oscar.apps.payment.exceptions import PaymentError
  18. Order = get_model('order', 'Order')
  19. OrderNote = get_model('order', 'OrderNote')
  20. ShippingAddress = get_model('order', 'ShippingAddress')
  21. Line = get_model('order', 'Line')
  22. ShippingEventType = get_model('order', 'ShippingEventType')
  23. PaymentEventType = get_model('order', 'PaymentEventType')
  24. EventHandler = get_class('order.processing', 'EventHandler')
  25. class OrderSummaryView(FormView):
  26. template_name = 'dashboard/orders/summary.html'
  27. form_class = forms.OrderSummaryForm
  28. def get(self, request, *args, **kwargs):
  29. if 'date_from' in request.GET or 'date_to' in request.GET:
  30. return self.post(request, *args, **kwargs)
  31. return super(OrderSummaryView, self).get(request, *args, **kwargs)
  32. def form_valid(self, form):
  33. ctx = self.get_context_data(form=form,
  34. filters=form.get_filters())
  35. return self.render_to_response(ctx)
  36. def get_form_kwargs(self):
  37. kwargs = super(OrderSummaryView, self).get_form_kwargs()
  38. kwargs['data'] = self.request.GET
  39. return kwargs
  40. def get_context_data(self, **kwargs):
  41. ctx = super(OrderSummaryView, self).get_context_data(**kwargs)
  42. filters = kwargs.get('filters', {})
  43. ctx.update(self.get_stats(filters))
  44. return ctx
  45. def get_stats(self, filters):
  46. orders = Order.objects.filter(**filters)
  47. stats = {
  48. 'total_orders': orders.count(),
  49. 'total_lines': Line.objects.filter(order__in=orders).count(),
  50. 'total_revenue': orders.aggregate(Sum('total_incl_tax'))['total_incl_tax__sum'] or D('0.00'),
  51. 'order_status_breakdown': orders.order_by('status').values('status').annotate(freq=Count('id'))
  52. }
  53. return stats
  54. class OrderListView(ListView, BulkEditMixin):
  55. model = Order
  56. context_object_name = 'orders'
  57. template_name = 'dashboard/orders/order_list.html'
  58. form_class = forms.OrderSearchForm
  59. base_description = 'All orders'
  60. paginate_by = 25
  61. description = ''
  62. actions = ('download_selected_orders',)
  63. current_view = 'dashboard:order-list'
  64. def get(self, request, *args, **kwargs):
  65. if 'order_number' in request.GET:
  66. try:
  67. order = Order.objects.get(number=request.GET['order_number'])
  68. except Order.DoesNotExist:
  69. pass
  70. else:
  71. return HttpResponseRedirect(reverse('dashboard:order-detail', kwargs={'number': order.number}))
  72. return super(OrderListView, self).get(request, *args, **kwargs)
  73. def get_queryset(self):
  74. """
  75. Build the queryset for this list and also update the title that
  76. describes the queryset
  77. """
  78. queryset = self.model.objects.all().order_by('-date_placed')
  79. self.description = self.base_description
  80. # Look for shortcut query filters
  81. if 'order_status' in self.request.GET:
  82. self.form = self.form_class()
  83. status = self.request.GET['order_status']
  84. if status.lower() == 'none':
  85. self.description = "Orders without an order status"
  86. status = None
  87. else:
  88. self.description = "Orders with status '%s'" % status
  89. return self.model.objects.filter(status=status)
  90. if 'order_number' not in self.request.GET:
  91. self.form = self.form_class()
  92. return queryset
  93. self.form = self.form_class(self.request.GET)
  94. if not self.form.is_valid():
  95. return queryset
  96. data = self.form.cleaned_data
  97. if data['order_number']:
  98. queryset = self.model.objects.filter(number__istartswith=data['order_number'])
  99. self.description = 'Orders with number starting with "%s"' % data['order_number']
  100. if data['name']:
  101. # If the value is two words, then assume they are first name and last name
  102. parts = data['name'].split()
  103. if len(parts) == 2:
  104. queryset = queryset.filter(Q(user__first_name__istartswith=parts[0]) |
  105. Q(user__last_name__istartswith=parts[1])).distinct()
  106. else:
  107. queryset = queryset.filter(Q(user__first_name__istartswith=data['name']) |
  108. Q(user__last_name__istartswith=data['name'])).distinct()
  109. self.description += " with customer name matching '%s'" % data['name']
  110. if data['product_title']:
  111. queryset = queryset.filter(lines__title__istartswith=data['product_title']).distinct()
  112. self.description += " including an item with title matching '%s'" % data['product_title']
  113. if data['product_id']:
  114. queryset = queryset.filter(Q(lines__upc=data['product_id']) |
  115. Q(lines__product_id=data['product_id'])).distinct()
  116. self.description += " including an item with ID '%s'" % data['product_id']
  117. if data['date_from'] and data['date_to']:
  118. # Add 24 hours to make search inclusive
  119. date_to = data['date_to'] + datetime.timedelta(days=1)
  120. queryset = queryset.filter(date_placed__gte=data['date_from']).filter(date_placed__lt=date_to)
  121. self.description += " placed between %s and %s" % (format_date(data['date_from']), format_date(data['date_to']))
  122. elif data['date_from']:
  123. queryset = queryset.filter(date_placed__gte=data['date_from'])
  124. self.description += " placed since %s" % format_date(data['date_from'])
  125. elif data['date_to']:
  126. date_to = data['date_to'] + datetime.timedelta(days=1)
  127. queryset = queryset.filter(date_placed__lt=date_to)
  128. self.description += " placed before %s" % format_date(data['date_to'])
  129. if data['voucher']:
  130. queryset = queryset.filter(discounts__voucher_code=data['voucher']).distinct()
  131. self.description += " using voucher '%s'" % data['voucher']
  132. if data['payment_method']:
  133. queryset = queryset.filter(sources__source_type__code=data['payment_method']).distinct()
  134. self.description += " paid for by %s" % data['payment_method']
  135. if data['status']:
  136. queryset = queryset.filter(status=data['status'])
  137. self.description += " with status %s" % data['status']
  138. return queryset
  139. def get_context_data(self, **kwargs):
  140. ctx = super(OrderListView, self).get_context_data(**kwargs)
  141. ctx['queryset_description'] = self.description
  142. ctx['form'] = self.form
  143. return ctx
  144. def is_csv_download(self):
  145. return self.request.GET.get('response_format', None) == 'csv'
  146. def get_paginate_by(self, queryset):
  147. return None if self.is_csv_download() else self.paginate_by
  148. def render_to_response(self, context):
  149. if self.is_csv_download():
  150. return self.download_selected_orders(self.request, context['object_list'])
  151. return super(OrderListView, self).render_to_response(context)
  152. def download_selected_orders(self, request, orders):
  153. response = HttpResponse(mimetype='text/csv')
  154. response['Content-Disposition'] = 'attachment; filename=orders.csv'
  155. writer = csv.writer(response, delimiter=',')
  156. meta_data = (('number', 'Order number'),
  157. ('value', 'Order value'),
  158. ('date', 'Date of purchase'),
  159. ('num_items', 'Number of items'),
  160. ('status', 'Order status'),
  161. ('shipping_address_name', 'Deliver to name'),
  162. ('billing_address_name', 'Bill to name'),
  163. )
  164. columns = SortedDict()
  165. for k, v in meta_data:
  166. columns[k] = v
  167. writer.writerow(columns.values())
  168. for order in orders:
  169. row = columns.copy()
  170. row['number'] = order.number
  171. row['value'] = order.total_incl_tax
  172. row['date'] = order.date_placed
  173. row['num_items'] = order.num_items
  174. row['status'] = order.status
  175. if order.shipping_address:
  176. row['shipping_address_name'] = order.shipping_address.name()
  177. else:
  178. row['shipping_address_name'] = ''
  179. if order.billing_address:
  180. row['billing_address_name'] = order.billing_address.name()
  181. else:
  182. row['billing_address_name'] = ''
  183. encoded_values = [unicode(value).encode('utf8') for value in row.values()]
  184. writer.writerow(encoded_values)
  185. return response
  186. class OrderDetailView(DetailView):
  187. model = Order
  188. context_object_name = 'order'
  189. template_name = 'dashboard/orders/order_detail.html'
  190. order_actions = ('save_note', 'delete_note', 'change_order_status',
  191. 'create_order_payment_event')
  192. line_actions = ('change_line_statuses', 'create_shipping_event',
  193. 'create_payment_event')
  194. def get_object(self):
  195. return get_object_or_404(self.model, number=self.kwargs['number'])
  196. def get_context_data(self, **kwargs):
  197. ctx = super(OrderDetailView, self).get_context_data(**kwargs)
  198. ctx['note_form'] = self.get_order_note_form()
  199. ctx['line_statuses'] = Line.all_statuses()
  200. ctx['shipping_event_types'] = ShippingEventType.objects.all()
  201. ctx['payment_event_types'] = PaymentEventType.objects.all()
  202. return ctx
  203. def get_order_note_form(self):
  204. post_data = None
  205. kwargs = {}
  206. if self.request.method == 'POST':
  207. post_data = self.request.POST
  208. note_id = self.kwargs.get('note_id', None)
  209. if note_id:
  210. note = get_object_or_404(OrderNote, order=self.object, id=note_id)
  211. kwargs['instance'] = note
  212. return forms.OrderNoteForm(post_data, **kwargs)
  213. def post(self, request, *args, **kwargs):
  214. self.object = self.get_object()
  215. order = self.object
  216. # Look for order-level action
  217. order_action = request.POST.get('order_action', '').lower()
  218. if order_action:
  219. if order_action not in self.order_actions:
  220. messages.error(self.request, "Invalid action")
  221. return self.reload_page_response()
  222. else:
  223. return getattr(self, order_action)(request, order)
  224. # Look for line-level action
  225. line_action = request.POST.get('line_action', '').lower()
  226. if line_action:
  227. if line_action not in self.line_actions:
  228. messages.error(self.request, "Invalid action")
  229. return self.reload_page_response()
  230. else:
  231. line_ids = request.POST.getlist('selected_line')
  232. line_quantities = request.POST.getlist('selected_line_qty')
  233. lines = order.lines.filter(id__in=line_ids)
  234. if lines.count() == 0:
  235. messages.error(self.request, "You must select some lines to act on")
  236. return self.reload_page_response()
  237. return getattr(self, line_action)(request, order, lines, line_quantities)
  238. messages.error(request, "No valid action submitted")
  239. return self.reload_page_response()
  240. def reload_page_response(self):
  241. return HttpResponseRedirect(reverse('dashboard:order-detail', kwargs={'number': self.object.number}))
  242. def save_note(self, request, order):
  243. form = self.get_order_note_form()
  244. success_msg = "Note saved"
  245. if form.is_valid():
  246. note = form.save(commit=False)
  247. note.user = request.user
  248. note.order = order
  249. note.save()
  250. messages.success(self.request, success_msg)
  251. return self.reload_page_response()
  252. ctx = self.get_context_data(note_form=form)
  253. return self.render_to_response(ctx)
  254. def delete_note(self, request, order):
  255. try:
  256. note = order.notes.get(id=request.POST.get('note_id', None))
  257. except ObjectDoesNotExist:
  258. messages.error(request, "Note cannot be deleted")
  259. else:
  260. messages.info(request, "Note deleted")
  261. note.delete()
  262. return self.reload_page_response()
  263. def change_order_status(self, request, order):
  264. new_status = request.POST['new_status'].strip()
  265. if not new_status:
  266. messages.error(request, "The new status '%s' is not valid" % new_status)
  267. return self.reload_page_response()
  268. if not new_status in order.available_statuses():
  269. messages.error(request, "The new status '%s' is not valid for this order" % new_status)
  270. return self.reload_page_response()
  271. handler = EventHandler()
  272. try:
  273. handler.handle_order_status_change(order, new_status)
  274. except PaymentError, e:
  275. messages.error(request, "Unable to change order status due to payment error: %s" % e)
  276. else:
  277. msg = "Order status changed from '%s' to '%s'" % (order.status, new_status)
  278. messages.info(request, msg)
  279. order.notes.create(user=request.user, message=msg,
  280. note_type=OrderNote.SYSTEM)
  281. return self.reload_page_response()
  282. def change_line_statuses(self, request, order, lines, quantities):
  283. new_status = request.POST['new_status'].strip()
  284. if not new_status:
  285. messages.error(request, "The new status '%s' is not valid" % new_status)
  286. return self.reload_page_response()
  287. errors = []
  288. for line in lines:
  289. if new_status not in line.available_statuses():
  290. errors.append("'%s' is not a valid new status for line %d" % (
  291. new_status, line.id))
  292. if errors:
  293. messages.error(request, "\n".join(errors))
  294. return self.reload_page_response()
  295. msgs = []
  296. for line in lines:
  297. msg = "Status of line %d changed from '%s' to '%s'" % (
  298. line.id, line.status, new_status)
  299. msgs.append(msg)
  300. line.set_status(new_status)
  301. message = "\n".join(msgs)
  302. messages.info(request, message)
  303. order.notes.create(user=request.user, message=message,
  304. note_type=OrderNote.SYSTEM)
  305. return self.reload_page_response()
  306. def create_shipping_event(self, request, order, lines, quantities):
  307. code = request.POST['shipping_event_type']
  308. try:
  309. event_type = ShippingEventType._default_manager.get(code=code)
  310. except ShippingEventType.DoesNotExist:
  311. messages.error(request, "The event type '%s' is not valid" % code)
  312. return self.reload_page_response()
  313. reference = request.POST.get('reference', None)
  314. try:
  315. EventHandler().handle_shipping_event(order, event_type, lines,
  316. quantities,
  317. reference=reference)
  318. except PaymentError, e:
  319. messages.error(request, "Unable to change order status due to payment error: %s" % e)
  320. else:
  321. messages.info(request, "Shipping event created")
  322. return self.reload_page_response()
  323. def create_order_payment_event(self, request, order):
  324. amount_str = request.POST.get('amount', None)
  325. try:
  326. amount = D(amount_str)
  327. except InvalidOperation:
  328. messages.error(request, "Please choose a valid amount")
  329. return self.reload_page_response()
  330. return self._create_payment_event(request, order)
  331. def _create_payment_event(self, request, order, amount, lines=None,
  332. quantities=None):
  333. code = request.POST['payment_event_type']
  334. try:
  335. event_type = PaymentEventType._default_manager.get(code=code)
  336. except PaymentEventType.DoesNotExist:
  337. messages.error(request, "The event type '%s' is not valid" % code)
  338. return self.reload_page_response()
  339. try:
  340. EventHandler().handle_payment_event(order, event_type, amount,
  341. lines, quantities)
  342. except PaymentError, e:
  343. messages.error(request, "Unable to change order status due to payment error: %s" % e)
  344. else:
  345. messages.info(request, "Payment event created")
  346. return self.reload_page_response()
  347. def create_payment_event(self, request, order, lines, quantities):
  348. amount_str = request.POST.get('amount', None)
  349. if not amount_str:
  350. amount = D('0.00')
  351. for line, quantity in zip(lines, quantities):
  352. amount += int(quantity) * line.line_price_incl_tax
  353. else:
  354. try:
  355. amount = D(amount_str)
  356. except InvalidOperation:
  357. messages.error(request, "Please choose a valid amount")
  358. return self.reload_page_response()
  359. return self._create_payment_event(request, order, amount, lines,
  360. quantities)
  361. class LineDetailView(DetailView):
  362. model = Line
  363. context_object_name = 'line'
  364. template_name = 'dashboard/orders/line_detail.html'
  365. def get_object(self, queryset=None):
  366. try:
  367. return self.model.objects.get(pk=self.kwargs['line_id'])
  368. except self.model.DoesNotExist:
  369. raise Http404()
  370. def get_context_data(self, **kwargs):
  371. ctx = super(LineDetailView, self).get_context_data(**kwargs)
  372. ctx['order'] = self.object.order
  373. return ctx
  374. def get_changes_between_models(model1, model2, excludes=[]):
  375. changes = {}
  376. for field in model1._meta.fields:
  377. if not (isinstance(field, (fields.AutoField, fields.related.RelatedField))
  378. or field.name in excludes):
  379. if field.value_from_object(model1) != field.value_from_object(model2):
  380. changes[field.verbose_name] = (field.value_from_object(model1),
  381. field.value_from_object(model2))
  382. return changes
  383. def get_change_summary(model1, model2):
  384. """
  385. Generate a summary of the changes between two address models
  386. """
  387. changes = get_changes_between_models(model1, model2, ['search_text'])
  388. change_descriptions = []
  389. for field, delta in changes.items():
  390. change_descriptions.append(u"%s changed from '%s' to '%s'" % (field, delta[0], delta[1]))
  391. return "\n".join(change_descriptions)
  392. class ShippingAddressUpdateView(UpdateView):
  393. model = ShippingAddress
  394. context_object_name = 'address'
  395. template_name = 'dashboard/orders/shippingaddress_form.html'
  396. form_class = forms.ShippingAddressForm
  397. def get_object(self):
  398. return get_object_or_404(self.model, order__number=self.kwargs['number'])
  399. def get_context_data(self, **kwargs):
  400. ctx = super(ShippingAddressUpdateView, self).get_context_data(**kwargs)
  401. ctx['order'] = self.object.order
  402. return ctx
  403. def form_valid(self, form):
  404. old_address = ShippingAddress.objects.get(id=self.object.id)
  405. response = super(ShippingAddressUpdateView, self).form_valid(form)
  406. changes = get_change_summary(old_address, self.object)
  407. if changes:
  408. msg = "Delivery address updated:\n%s" % changes
  409. self.object.order.notes.create(user=self.request.user, message=msg,
  410. note_type=OrderNote.SYSTEM)
  411. return response
  412. def get_success_url(self):
  413. messages.info(self.request, "Delivery address updated")
  414. return reverse('dashboard:order-detail', kwargs={'number': self.object.order.number, })