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

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