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 19KB

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