import csv import datetime from decimal import Decimal as D, InvalidOperation from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist from django.db.models.loading import get_model from django.db.models import fields, Q, Sum, Count from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404 from django.template.defaultfilters import date as format_date from django.utils.datastructures import SortedDict from django.views.generic import ListView, DetailView, UpdateView, FormView from oscar.core.loading import get_class from oscar.apps.dashboard.orders import forms from oscar.views.generic import BulkEditMixin from oscar.apps.payment.exceptions import PaymentError from oscar.apps.order.exceptions import InvalidShippingEvent, InvalidStatus Order = get_model('order', 'Order') OrderNote = get_model('order', 'OrderNote') ShippingAddress = get_model('order', 'ShippingAddress') Line = get_model('order', 'Line') ShippingEventType = get_model('order', 'ShippingEventType') PaymentEventType = get_model('order', 'PaymentEventType') EventHandler = get_class('order.processing', 'EventHandler') class OrderStatsView(FormView): template_name = 'dashboard/orders/statistics.html' form_class = forms.OrderStatsForm def get(self, request, *args, **kwargs): return self.post(request, *args, **kwargs) def form_valid(self, form): ctx = self.get_context_data(form=form, filters=form.get_filters()) return self.render_to_response(ctx) def get_form_kwargs(self): kwargs = super(OrderStatsView, self).get_form_kwargs() kwargs['data'] = self.request.GET return kwargs def get_context_data(self, **kwargs): ctx = super(OrderStatsView, self).get_context_data(**kwargs) filters = kwargs.get('filters', {}) ctx.update(self.get_stats(filters)) ctx['title'] = kwargs['form'].get_filter_description() return ctx def get_stats(self, filters): orders = Order.objects.filter(**filters) stats = { 'total_orders': orders.count(), 'total_lines': Line.objects.filter(order__in=orders).count(), 'total_revenue': orders.aggregate(Sum('total_incl_tax'))['total_incl_tax__sum'] or D('0.00'), 'order_status_breakdown': orders.order_by('status').values('status').annotate(freq=Count('id')) } return stats class OrderListView(ListView, BulkEditMixin): model = Order context_object_name = 'orders' template_name = 'dashboard/orders/order_list.html' form_class = forms.OrderSearchForm desc_template = _("%(main_filter)s %(name_filter)s %(title_filter)s" "%(upc_filter)s %(sku_filter)s %(date_filter)s" "%(voucher_filter)s %(payment_filter)s %(status_filter)s") paginate_by = 25 description = '' actions = ('download_selected_orders',) current_view = 'dashboard:order-list' def get(self, request, *args, **kwargs): if 'order_number' in request.GET and request.GET.get('response_format', None) == 'html': try: order = Order.objects.get(number=request.GET['order_number']) except Order.DoesNotExist: pass else: return HttpResponseRedirect(reverse('dashboard:order-detail', kwargs={'number': order.number})) return super(OrderListView, self).get(request, *args, **kwargs) def get_queryset(self): """ Build the queryset for this list and also update the title that describes the queryset """ queryset = self.model.objects.all().order_by('-date_placed') queryset = self.sort_queryset(queryset) desc_ctx = { 'main_filter': _('All orders'), 'name_filter': '', 'title_filter': '', 'upc_filter': '', 'sku_filter': '', 'date_filter': '', 'voucher_filter': '', 'payment_filter': '', 'status_filter': '', } # Look for shortcut query filters if 'order_status' in self.request.GET: self.form = self.form_class() status = self.request.GET['order_status'] if status.lower() == 'none': desc_ctx['main_filter'] = _("Orders without an order status") status = None else: desc_ctx['main_filter'] = _("Orders with status '%s'") % status self.description = self.desc_template % desc_ctx return self.model.objects.filter(status=status) if 'order_number' not in self.request.GET: self.description = self.desc_template % desc_ctx self.form = self.form_class() return queryset self.form = self.form_class(self.request.GET) if not self.form.is_valid(): return queryset data = self.form.cleaned_data if data['order_number']: queryset = self.model.objects.filter(number__istartswith=data['order_number']) desc_ctx['main_filter'] = _('Orders with number starting with "%s"') % data['order_number'] if data['name']: # If the value is two words, then assume they are first name and last name parts = data['name'].split() if len(parts) == 2: queryset = queryset.filter(Q(user__first_name__istartswith=parts[0]) | Q(user__last_name__istartswith=parts[1])).distinct() else: queryset = queryset.filter(Q(user__first_name__istartswith=data['name']) | Q(user__last_name__istartswith=data['name'])).distinct() desc_ctx['name_filter'] = _(" with customer name matching '%s'") % data['name'] if data['product_title']: queryset = queryset.filter(lines__title__istartswith=data['product_title']).distinct() desc_ctx['title_filter'] = _(" including an item with title matching '%s'") % data['product_title'] if data['upc']: queryset = queryset.filter(lines__upc=data['upc']) desc_ctx['upc_filter'] = _(" including an item with UPC '%s'") % data['upc'] if data['partner_sku']: queryset = queryset.filter(lines__partner_sku=data['partner_sku']) desc_ctx['upc_filter'] = _(" including an item with ID '%s'") % data['partner_sku'] if data['date_from'] and data['date_to']: # Add 24 hours to make search inclusive date_to = data['date_to'] + datetime.timedelta(days=1) queryset = queryset.filter(date_placed__gte=data['date_from']).filter(date_placed__lt=date_to) desc_ctx['date_filter'] = _(" placed between %(start_date)s and %(end_date)s") % { 'start_date': format_date(data['date_from']), 'end_date': format_date(data['date_to'])} elif data['date_from']: queryset = queryset.filter(date_placed__gte=data['date_from']) desc_ctx['date_filter'] = _(" placed since %s") % format_date(data['date_from']) elif data['date_to']: date_to = data['date_to'] + datetime.timedelta(days=1) queryset = queryset.filter(date_placed__lt=date_to) desc_ctx['date_filter'] = _(" placed before %s") % format_date(data['date_to']) if data['voucher']: queryset = queryset.filter(discounts__voucher_code=data['voucher']).distinct() desc_ctx['voucher_filter'] = _(" using voucher '%s'") % data['voucher'] if data['payment_method']: queryset = queryset.filter(sources__source_type__code=data['payment_method']).distinct() desc_ctx['payment_filter'] = _(" paid for by %s") % data['payment_method'] if data['status']: queryset = queryset.filter(status=data['status']) desc_ctx['status_filter'] = _(" with status %s") % data['status'] return queryset def sort_queryset(self, queryset): sort = self.request.GET.get('sort', None) allowed_sorts = ['number',] if sort in allowed_sorts: direction = self.request.GET.get('dir', 'desc') sort = ('-' if direction == 'desc' else '') + sort queryset = queryset.order_by(sort) return queryset def get_context_data(self, **kwargs): ctx = super(OrderListView, self).get_context_data(**kwargs) ctx['queryset_description'] = self.description ctx['form'] = self.form return ctx def is_csv_download(self): return self.request.GET.get('response_format', None) == 'csv' def get_paginate_by(self, queryset): return None if self.is_csv_download() else self.paginate_by def render_to_response(self, context): if self.is_csv_download(): return self.download_selected_orders(self.request, context['object_list']) return super(OrderListView, self).render_to_response(context) def get_download_filename(self, request): return 'orders.csv' def download_selected_orders(self, request, orders): response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename=%s' % self.get_download_filename(request) writer = csv.writer(response, delimiter=',') meta_data = (('number', _('Order number')), ('value', _('Order value')), ('date', _('Date of purchase')), ('num_items', _('Number of items')), ('status', _('Order status')), ('customer', _('Customer email address')), ('shipping_address_name', _('Deliver to name')), ('billing_address_name', _('Bill to name')), ) columns = SortedDict() for k, v in meta_data: columns[k] = v writer.writerow(columns.values()) for order in orders: row = columns.copy() row['number'] = order.number row['value'] = order.total_incl_tax row['date'] = format_date(order.date_placed, 'DATETIME_FORMAT') row['num_items'] = order.num_items row['status'] = order.status row['customer'] = order.email if order.shipping_address: row['shipping_address_name'] = order.shipping_address.name() else: row['shipping_address_name'] = '' if order.billing_address: row['billing_address_name'] = order.billing_address.name() else: row['billing_address_name'] = '' encoded_values = [unicode(value).encode('utf8') for value in row.values()] writer.writerow(encoded_values) return response class OrderDetailView(DetailView): model = Order context_object_name = 'order' template_name = 'dashboard/orders/order_detail.html' order_actions = ('save_note', 'delete_note', 'change_order_status', 'create_order_payment_event') line_actions = ('change_line_statuses', 'create_shipping_event', 'create_payment_event') def get_object(self): return get_object_or_404(self.model, number=self.kwargs['number']) def get_context_data(self, **kwargs): ctx = super(OrderDetailView, self).get_context_data(**kwargs) ctx['active_tab'] = kwargs.get('active_tab', 'lines') ctx['note_form'] = self.get_order_note_form() ctx['line_statuses'] = Line.all_statuses() ctx['shipping_event_types'] = ShippingEventType.objects.all() ctx['payment_event_types'] = PaymentEventType.objects.all() return ctx def get_order_note_form(self): post_data = None kwargs = {} if self.request.method == 'POST': post_data = self.request.POST note_id = self.kwargs.get('note_id', None) if note_id: note = get_object_or_404(OrderNote, order=self.object, id=note_id) if note.is_editable(): kwargs['instance'] = note return forms.OrderNoteForm(post_data, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() order = self.object # Look for order-level action order_action = request.POST.get('order_action', '').lower() if order_action: if order_action not in self.order_actions: messages.error(self.request, _("Invalid action")) return self.reload_page_response() else: return getattr(self, order_action)(request, order) # Look for line-level action line_action = request.POST.get('line_action', '').lower() if line_action: if line_action not in self.line_actions: messages.error(self.request, "Invalid action") return self.reload_page_response() else: line_ids = request.POST.getlist('selected_line') line_quantities = [int(qty) for qty in request.POST.getlist('selected_line_qty')] lines = order.lines.filter(id__in=line_ids) if lines.count() == 0: messages.error(self.request, _("You must select some lines to act on")) return self.reload_page_response() return getattr(self, line_action)(request, order, lines, line_quantities) messages.error(request, _("No valid action submitted")) return self.reload_page_response() def reload_page_response(self, fragment=None): url = reverse('dashboard:order-detail', kwargs={'number': self.object.number}) if fragment: url += '#' + fragment return HttpResponseRedirect(url) def save_note(self, request, order): form = self.get_order_note_form() success_msg = _("Note saved") if form.is_valid(): note = form.save(commit=False) note.user = request.user note.order = order note.save() messages.success(self.request, success_msg) return self.reload_page_response(fragment='notes') ctx = self.get_context_data(note_form=form, active_tab='notes') return self.render_to_response(ctx) def delete_note(self, request, order): try: note = order.notes.get(id=request.POST.get('note_id', None)) except ObjectDoesNotExist: messages.error(request, _("Note cannot be deleted")) else: messages.info(request, _("Note deleted")) note.delete() return self.reload_page_response() def change_order_status(self, request, order): new_status = request.POST['new_status'].strip() if not new_status: messages.error(request, _("The new status '%s' is not valid") % new_status) return self.reload_page_response() if not new_status in order.available_statuses(): messages.error(request, _("The new status '%s' is not valid for this order") % new_status) return self.reload_page_response() handler = EventHandler() try: handler.handle_order_status_change(order, new_status) except PaymentError, e: messages.error(request, _("Unable to change order status due to payment error: %s") % e) else: msg = _("Order status changed from '%(old_status)s' to '%(new_status)s'") % { 'old_status': order.status, 'new_status': new_status} messages.info(request, msg) order.notes.create(user=request.user, message=msg, note_type=OrderNote.SYSTEM) return self.reload_page_response(fragment='activity') def change_line_statuses(self, request, order, lines, quantities): new_status = request.POST['new_status'].strip() if not new_status: messages.error(request, _("The new status '%s' is not valid") % new_status) return self.reload_page_response() errors = [] for line in lines: if new_status not in line.available_statuses(): errors.append(_("'%(status)s' is not a valid new status for line %(line_id)d") % { 'status': new_status, 'line_id': line.id}) if errors: messages.error(request, "\n".join(errors)) return self.reload_page_response() msgs = [] for line in lines: msg = _("Status of line #%(line_id)d changed from '%(old_status)s' to '%(new_status)s'") % { 'line_id': line.id, 'old_status': line.status, 'new_status': new_status} msgs.append(msg) line.set_status(new_status) message = "\n".join(msgs) messages.info(request, message) order.notes.create(user=request.user, message=message, note_type=OrderNote.SYSTEM) return self.reload_page_response() def create_shipping_event(self, request, order, lines, quantities): code = request.POST['shipping_event_type'] try: event_type = ShippingEventType._default_manager.get(code=code) except ShippingEventType.DoesNotExist: messages.error(request, _("The event type '%s' is not valid") % code) return self.reload_page_response() reference = request.POST.get('reference', None) try: EventHandler().handle_shipping_event(order, event_type, lines, quantities, reference=reference) except InvalidShippingEvent, e: messages.error(request, _("Unable to create shipping event: %s") % e) except InvalidStatus, e: messages.error(request, _("Unable to create shipping event: %s") % e) except PaymentError, e: messages.error(request, _("Unable to create shipping event due to payment error: %s") % e) else: messages.success(request, _("Shipping event created")) return self.reload_page_response() def create_order_payment_event(self, request, order): amount_str = request.POST.get('amount', None) try: amount = D(amount_str) except InvalidOperation: messages.error(request, _("Please choose a valid amount")) return self.reload_page_response() return self._create_payment_event(request, order, amount) def _create_payment_event(self, request, order, amount, lines=None, quantities=None): code = request.POST['payment_event_type'] try: event_type = PaymentEventType._default_manager.get(code=code) except PaymentEventType.DoesNotExist: messages.error(request, _("The event type '%s' is not valid") % code) return self.reload_page_response() try: EventHandler().handle_payment_event(order, event_type, amount, lines, quantities) except PaymentError, e: messages.error(request, _("Unable to change order status due to payment error: %s") % e) else: messages.info(request, _("Payment event created")) return self.reload_page_response() def create_payment_event(self, request, order, lines, quantities): amount_str = request.POST.get('amount', None) # If no amount passed, then we add up the total of the selected lines if not amount_str: amount = D('0.00') for line, quantity in zip(lines, quantities): amount += int(quantity) * line.line_price_incl_tax else: try: amount = D(amount_str) except InvalidOperation: messages.error(request, _("Please choose a valid amount")) return self.reload_page_response() return self._create_payment_event(request, order, amount, lines, quantities) class LineDetailView(DetailView): model = Line context_object_name = 'line' template_name = 'dashboard/orders/line_detail.html' def get_object(self, queryset=None): try: return self.model.objects.get(pk=self.kwargs['line_id']) except self.model.DoesNotExist: raise Http404() def get_context_data(self, **kwargs): ctx = super(LineDetailView, self).get_context_data(**kwargs) ctx['order'] = self.object.order return ctx def get_changes_between_models(model1, model2, excludes=[]): changes = {} for field in model1._meta.fields: if not (isinstance(field, (fields.AutoField, fields.related.RelatedField)) or field.name in excludes): if field.value_from_object(model1) != field.value_from_object(model2): changes[field.verbose_name] = (field.value_from_object(model1), field.value_from_object(model2)) return changes def get_change_summary(model1, model2): """ Generate a summary of the changes between two address models """ changes = get_changes_between_models(model1, model2, ['search_text']) change_descriptions = [] for field, delta in changes.items(): change_descriptions.append(_("%(field)s changed from '%(old_value)s' to '%(new_value)s'") % { 'field': field, 'old_value': delta[0], 'new_value': delta[1]}) return "\n".join(change_descriptions) class ShippingAddressUpdateView(UpdateView): model = ShippingAddress context_object_name = 'address' template_name = 'dashboard/orders/shippingaddress_form.html' form_class = forms.ShippingAddressForm def get_object(self): return get_object_or_404(self.model, order__number=self.kwargs['number']) def get_context_data(self, **kwargs): ctx = super(ShippingAddressUpdateView, self).get_context_data(**kwargs) ctx['order'] = self.object.order return ctx def form_valid(self, form): old_address = ShippingAddress.objects.get(id=self.object.id) response = super(ShippingAddressUpdateView, self).form_valid(form) changes = get_change_summary(old_address, self.object) if changes: msg = _("Delivery address updated:\n%s") % changes self.object.order.notes.create(user=self.request.user, message=msg, note_type=OrderNote.SYSTEM) return response def get_success_url(self): messages.info(self.request, _("Delivery address updated")) return reverse('dashboard:order-detail', kwargs={'number': self.object.order.number, })