Настройка способов доставки в django-oscar

Настройка способов доставки в django-oscar
  • Опубликовано:
  • Теги: django-oscar

Проблема

Недавно заказчик попросил добавить около десятка разных способов доставки, в основном почтой. Причем ожидается, что эти способы будут меняться в будущем. Не суть важно почему, рассказываю какое было выбрано решение.

Кроме того, немного поменяли последовательность оформления заказа. Изначально была очередность:

  1. Указание адреса (выбор из списка имеющихся адресов или добавление нового).
  2. Выбор способа доставки.
  3. Выбор способа оплаты.
  4. Подтверждение.

Но если клиенту не нужна доставка и он выберет самовывоз, то шаг с указанием адреса лишний, во-первых. Во-вторых, он должен идти после выбора способа доставки, а не наоборот.

Опишу, как эта задача была решена.

Решение

Делаем форк приложения shipping:

./manage.py oscar_fork_app shipping apps/

Добавляем приложение в конец settings.py, например так:

INSTALLED_APPS = INSTALLED_APPS + get_core_apps(
      ['apps.shipping'])

Допустим, у нас будет два постоянных способа доставки: самовывоз и курьерская доставка. В папке shipping создаем файлы methods.py и repository.py:

# apps/shipping/methods.py

from decimal import Decimal as D
from django.utils.translation import ugettext_lazy as _
from oscar.apps.shipping import methods


class SelfPickup(methods.Free):
    code = 'self-pickup'
    name = _('Self pickup')


class Courier(methods.FixedPrice):
    code = 'courier'
    name = _('Courier shipping')

    charge_excl_tax = D('350.00')
    charge_incl_tax = D('350.00')

Как видите, здесь указаны два способа доставки, которые мы укажем в классе Repository далее.

# apps/shipping/repository.py

from oscar.apps.shipping import repository
from . import methods


class Repository(repository.Repository):
    methods = [
        methods.SelfPickup(),
        methods.Courier(),
    ]

Добавим модель, наследующую одну из типовых доставок из коробки oscar.

# apps/shipping/models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _
from oscar.apps.shipping.abstract_models import AbstractOrderAndItemCharges


class StandardMethod(AbstractOrderAndItemCharges):
    """Модель с обычным набором атрибутов"""

    class Meta(AbstractOrderAndItemCharges.Meta):
        verbose_name = _('Shipping method')
        verbose_name_plural = _('Shipping methods')

from oscar.apps.shipping.models import *  # noqa

Способы доставки в будущем потребуется менять, но намного реже, чем остальные компоненты. Поэтому пока «сердито и дешево» добавим интерфейс для изменения способов доставки в панель администрирования Django.

# apps/shipping/admin.py

from django.contrib import admin

from apps.shipping.models import StandardMethod


class StandardMethodAdmin(admin.ModelAdmin):
    list_display = ('name',)
admin.site.register(StandardMethod, StandardMethodAdmin)

from oscar.apps.shipping.admin import *  # noqa
admin.site.unregister(OrderAndItemCharges)
admin.site.unregister(WeightBased)

Как видите, мы отключили лишние модели способов доставки (если нужны, не отключайте). При желании и необходимости, можно добавить приложение в админку oscar.

Для изменения порядка очередности нам нужно внести изменения:

  1. В шаблоне templates/checkout/nav.html, где показана навигация по шагам оформления. Там просто, описывать не буду.
  2. В модуле apps/checkout/views.py поправить переадресацию шагов с учетом новой очередности.

Со вторым пунктом пришлось повозиться.

Во-первых, изменим первый шаг процесса оформления:

# apps/checkout/views.py

class IndexView(CoreIndexView):
    success_url = reverse_lazy('checkout:shipping-method')

Во-вторых, на шаге выбора способа доставки проверим, нужно ли выбирать адрес. Если самовывоз, то не нужно, тогда переходим к способу оплаты (перед return добавляем проверку)

class ShippingMethodView(CoreShippingMethodView):
    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)
        if not self.is_valid_shipping_method(method_code):
            messages.error(request, _("Your submitted shipping method is not"
                                      " permitted"))
            return redirect('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)

        if method_code != 'self-pickup':
            return redirect('checkout:shipping-address')
        return self.get_success_response()

И, наконец, при сохранении адреса oscar сбрасывает информацию о доставке и перезаписывает ее заново. Поэтому перед этим нам надо временно сохранить способ доставки, затем записать адрес и добавить к нему способ доставки:

class ShippingAddressView(CoreShippingAddressView):
    success_url = reverse_lazy('checkout:payment-method')

    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'], user=self.request.user)
            action = self.request.POST.get('action', None)
            if action == 'ship_to':
                # User has selected a previous address to ship to

                # Получаем код способа доставки, чтобы после
                # сброса записать его снова
                method_code = self.checkout_session.shipping_method_code(
                    self.request.basket)
                self.checkout_session.ship_to_user_address(address)
                if method_code:
                    self.checkout_session.use_shipping_method(method_code)
                return redirect(self.get_success_url())
            else:
                return http.HttpResponseBadRequest()
        else:
            return super(ShippingAddressView, self).post(
                request, *args, **kwargs)

Вот в таком виде уже работает. Еще момент — при правке шаблона навигации нужно указать проследить, чтобы активные шаги правильно отмечались соответствующими стилями.