
Проблема
Недавно заказчик попросил добавить около десятка разных способов доставки, в основном почтой. Причем ожидается, что эти способы будут меняться в будущем. Не суть важно почему, рассказываю какое было выбрано решение.
Кроме того, немного поменяли последовательность оформления заказа. Изначально была очередность:
- Указание адреса (выбор из списка имеющихся адресов или добавление нового).
- Выбор способа доставки.
- Выбор способа оплаты.
- Подтверждение.
Но если клиенту не нужна доставка и он выберет самовывоз, то шаг с указанием адреса лишний, во-первых. Во-вторых, он должен идти после выбора способа доставки, а не наоборот.
Опишу, как эта задача была решена.
Решение
Делаем форк приложения 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.
Для изменения порядка очередности нам нужно внести изменения:
- В шаблоне templates/checkout/nav.html, где показана навигация по шагам оформления. Там просто, описывать не буду.
- В модуле 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)
Вот в таком виде уже работает. Еще момент — при правке шаблона навигации нужно указать проследить, чтобы активные шаги правильно отмечались соответствующими стилями.