Repository: saleor/saleor
Pull request: Merge Checkout with Cart
I want to merge this change because it closes #2004. Checkout class is removed and all data needed for that process are now saved in database within Cart model. Also, the business logic standing behind handling shipping and billing addresses in checkout is simplified.

### TODO's
* [x] Simplify and refactor existing tests
* [x] Test new functionalities
* [x] Merge checkout and cart folders, keep distinct urls

### Pull Request Checklist

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [ ] All visible strings are translated with proper context.
1. [ ] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [ ] The changes are tested.
1. [ ] The code is documented (docstrings, project documentation).
1. [ ] Python code quality checks pass: `pycodestyle`, `pydocstyle`, `pylint`.
1. [ ] JavaScript code quality checks pass: `eslint`.


Problem statement:
Saving checkouts in database
Checkout should have a dedicated model that would be stored in the database. This will allow other features such as:
- sharing checkouts
- creating better checkout API (multiple calls to create/edit checkout)
- tracking abandoned checkouts

Hints from issue discussion:
As we talked internally, we could try with moving fields from `Checkout` object to `Cart` model.

Commit message: Fix Django 1.11 compatibility

Patch to review:
diff --git a/saleor/account/forms.py b/saleor/account/forms.py
index 63cd65f72d0..564fd41a9ef 100644
--- a/saleor/account/forms.py
+++ b/saleor/account/forms.py
@@ -25,10 +25,8 @@ def get_address_form(
     address_form_class = get_address_form_class(country_code)
 
     if not preview and instance is not None:
-        address_form_class = get_address_form_class(
-            instance.country.code)
-        address_form = address_form_class(
-            data, instance=instance, **kwargs)
+        address_form_class = get_address_form_class(instance.country.code)
+        address_form = address_form_class(data, instance=instance, **kwargs)
     else:
         initial_address = (
             initial if not preview
diff --git a/saleor/account/migrations/0019_auto_20180528_1205.py b/saleor/account/migrations/0019_auto_20180528_1205.py
new file mode 100644
index 00000000000..801cb732176
--- /dev/null
+++ b/saleor/account/migrations/0019_auto_20180528_1205.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.3 on 2018-05-28 17:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0018_auto_20180426_0641'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='user',
+            name='addresses',
+            field=models.ManyToManyField(blank=True, related_name='user_addresses', to='account.Address'),
+        ),
+    ]
diff --git a/saleor/account/models.py b/saleor/account/models.py
index 7368a580fef..228392b2a48 100644
--- a/saleor/account/models.py
+++ b/saleor/account/models.py
@@ -92,7 +92,8 @@ def create_superuser(self, email, password=None, **extra_fields):
 
 class User(PermissionsMixin, AbstractBaseUser):
     email = models.EmailField(unique=True)
-    addresses = models.ManyToManyField(Address, blank=True)
+    addresses = models.ManyToManyField(
+        Address, blank=True, related_name='user_addresses')
     is_staff = models.BooleanField(default=False)
     is_active = models.BooleanField(default=True)
     note = models.TextField(null=True, blank=True)
diff --git a/saleor/account/utils.py b/saleor/account/utils.py
index f363062a794..a10dde69648 100644
--- a/saleor/account/utils.py
+++ b/saleor/account/utils.py
@@ -1,13 +1,15 @@
-def store_user_address(user, address, billing=False, shipping=False):
-    data = address.as_data()
-    entry = user.addresses.get_or_create(**data)[0]
-    changed = False
-    if billing and not user.default_billing_address_id:
-        user.default_billing_address = entry
-        changed = True
-    if shipping and not user.default_shipping_address_id:
-        user.default_shipping_address = entry
-        changed = True
-    if changed:
-        user.save()
-    return entry
+from ..checkout import AddressType
+
+
+def store_user_address(user, address, address_type):
+    """Add address to user address book and set as default one."""
+    address, _ = user.addresses.get_or_create(**address.as_data())
+
+    if address_type == AddressType.BILLING:
+        if not user.default_billing_address:
+            user.default_billing_address = address
+            user.save(update_fields=['default_billing_address'])
+    elif address_type == AddressType.SHIPPING:
+        if not user.default_shipping_address:
+            user.default_shipping_address = address
+            user.save(update_fields=['default_shipping_address'])
diff --git a/saleor/account/views.py b/saleor/account/views.py
index 6c222bb857d..1bbdb346ba6 100755
--- a/saleor/account/views.py
+++ b/saleor/account/views.py
@@ -8,7 +8,7 @@
 from django.urls import reverse, reverse_lazy
 from django.utils.translation import pgettext, ugettext_lazy as _
 
-from ..cart.utils import find_and_assign_anonymous_cart
+from ..checkout.utils import find_and_assign_anonymous_cart
 from ..core.utils import get_paginator_items
 from .forms import (
     ChangePasswordForm, LoginForm, PasswordResetForm, SignupForm,
diff --git a/saleor/cart/__init__.py b/saleor/cart/__init__.py
deleted file mode 100644
index 3ebdcb37ab4..00000000000
--- a/saleor/cart/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import logging
-
-from django.utils.translation import pgettext_lazy
-
-logger = logging.getLogger(__name__)
-
-
-class CartStatus:
-    """Enum of possible cart statuses."""
-
-    OPEN = 'open'
-    CANCELED = 'canceled'
-
-    CHOICES = [
-        (OPEN, pgettext_lazy(
-            'cart status', 'Open - currently active')),
-        (CANCELED, pgettext_lazy(
-            'cart status', 'Canceled - canceled by user'))]
diff --git a/saleor/cart/forms.py b/saleor/cart/forms.py
deleted file mode 100644
index 94b843bb90b..00000000000
--- a/saleor/cart/forms.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Cart-related forms and fields."""
-from django import forms
-from django.conf import settings
-from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist
-from django.utils.translation import npgettext_lazy, pgettext_lazy
-from django_countries.fields import LazyTypedChoiceField, countries
-
-from ..core.exceptions import InsufficientStock
-from ..shipping.utils import get_shipment_options
-
-
-class QuantityField(forms.IntegerField):
-    """A specialized integer field with initial quantity and min/max values."""
-
-    def __init__(self, **kwargs):
-        super().__init__(
-            min_value=0, max_value=settings.MAX_CART_LINE_QUANTITY,
-            initial=1, **kwargs)
-
-
-class AddToCartForm(forms.Form):
-    """Add-to-cart form.
-
-    Allows selection of a product variant and quantity.
-
-    The save method adds it to the cart.
-    """
-
-    quantity = QuantityField(
-        label=pgettext_lazy('Add to cart form field label', 'Quantity'))
-    error_messages = {
-        'not-available': pgettext_lazy(
-            'Add to cart form error',
-            'Sorry. This product is currently not available.'),
-        'empty-stock': pgettext_lazy(
-            'Add to cart form error',
-            'Sorry. This product is currently out of stock.'),
-        'variant-does-not-exists': pgettext_lazy(
-            'Add to cart form error',
-            'Oops. We could not find that product.'),
-        'insufficient-stock': npgettext_lazy(
-            'Add to cart form error',
-            'Only %d remaining in stock.',
-            'Only %d remaining in stock.')}
-
-    def __init__(self, *args, **kwargs):
-        self.cart = kwargs.pop('cart')
-        self.product = kwargs.pop('product')
-        self.discounts = kwargs.pop('discounts', ())
-        self.taxes = kwargs.pop('taxes', {})
-        super().__init__(*args, **kwargs)
-
-    def clean(self):
-        """Clean the form.
-
-        Makes sure the total quantity in cart (taking into account what was
-        already there) does not exceed available quantity.
-        """
-        cleaned_data = super().clean()
-        quantity = cleaned_data.get('quantity')
-        if quantity is None:
-            return cleaned_data
-        try:
-            product_variant = self.get_variant(cleaned_data)
-        except ObjectDoesNotExist:
-            msg = self.error_messages['variant-does-not-exists']
-            self.add_error(NON_FIELD_ERRORS, msg)
-        else:
-            cart_line = self.cart.get_line(product_variant)
-            used_quantity = cart_line.quantity if cart_line else 0
-            new_quantity = quantity + used_quantity
-            try:
-                product_variant.check_quantity(new_quantity)
-            except InsufficientStock as e:
-                remaining = e.item.quantity_available - used_quantity
-                if remaining:
-                    msg = self.error_messages['insufficient-stock']
-                    self.add_error('quantity', msg % remaining)
-                else:
-                    msg = self.error_messages['empty-stock']
-                    self.add_error('quantity', msg)
-        return cleaned_data
-
-    def save(self):
-        """Add the selected product variant and quantity to the cart."""
-        product_variant = self.get_variant(self.cleaned_data)
-        return self.cart.add(variant=product_variant,
-                             quantity=self.cleaned_data['quantity'])
-
-    def get_variant(self, cleaned_data):
-        """Return a product variant that matches submitted values.
-
-        This allows specialized implementations to select the variant based on
-        multiple fields (like size and color) instead of having a single
-        variant selection widget.
-        """
-        raise NotImplementedError()
-
-
-class ReplaceCartLineForm(AddToCartForm):
-    """Replace quantity in cart form.
-
-    Similar to AddToCartForm but its save method replaces the quantity.
-    """
-
-    def __init__(self, *args, **kwargs):
-        self.variant = kwargs.pop('variant')
-        kwargs['product'] = self.variant.product
-        super().__init__(*args, **kwargs)
-        self.cart_line = self.cart.get_line(self.variant)
-        self.fields['quantity'].widget.attrs = {
-            'min': 1, 'max': settings.MAX_CART_LINE_QUANTITY}
-
-    def clean_quantity(self):
-        """Clean the quantity field.
-
-        Checks if target quantity does not exceed the currently available
-        quantity.
-        """
-        quantity = self.cleaned_data['quantity']
-        try:
-            self.variant.check_quantity(quantity)
-        except InsufficientStock as e:
-            msg = self.error_messages['insufficient-stock']
-            raise forms.ValidationError(
-                msg % e.item.quantity_available)
-        return quantity
-
-    def clean(self):
-        """Clean the form skipping the add-to-form checks."""
-        # explicitly skip parent's implementation
-        # pylint: disable=E1003
-        return super(AddToCartForm, self).clean()
-
-    def get_variant(self, cleaned_data):
-        """Return the matching variant.
-
-        In this case we explicitly know the variant as we're modifying an
-        existing line in cart.
-        """
-        return self.variant
-
-    def save(self):
-        """Replace the selected product's quantity in cart."""
-        product_variant = self.get_variant(self.cleaned_data)
-        return self.cart.add(product_variant, self.cleaned_data['quantity'],
-                             replace=True)
-
-
-class CountryForm(forms.Form):
-    """Country selection form."""
-
-    country = LazyTypedChoiceField(
-        label=pgettext_lazy('Country form field label', 'Country'),
-        choices=countries)
-
-    def __init__(self, *args, **kwargs):
-        self.taxes = kwargs.pop('taxes', {})
-        super().__init__(*args, **kwargs)
-
-    def get_shipment_options(self):
-        """Return a list of shipping methods for the selected country."""
-        code = self.cleaned_data['country']
-        return get_shipment_options(code, self.taxes)
diff --git a/saleor/cart/models.py b/saleor/cart/models.py
deleted file mode 100644
index be1e1d53879..00000000000
--- a/saleor/cart/models.py
+++ /dev/null
@@ -1,244 +0,0 @@
-"""Cart-related ORM models."""
-from collections import namedtuple
-from decimal import Decimal
-from uuid import uuid4
-
-from django.conf import settings
-from django.core.validators import MaxValueValidator, MinValueValidator
-from django.db import models
-from django.utils.encoding import smart_str
-from django.utils.timezone import now
-from django_prices.models import MoneyField
-from jsonfield import JSONField
-from prices import sum as sum_prices
-
-from . import CartStatus, logger
-
-CENTS = Decimal('0.01')
-SimpleCart = namedtuple('SimpleCart', ('quantity', 'total', 'token'))
-
-
-def find_open_cart_for_user(user):
-    """Find an open cart for the given user."""
-    carts = user.carts.open()
-    if len(carts) > 1:
-        logger.warning('%s has more than one open basket', user)
-        for cart in carts[1:]:
-            cart.change_status(CartStatus.CANCELED)
-    return carts.first()
-
-
-class CartQueryset(models.QuerySet):
-    """A specialized queryset for dealing with carts."""
-
-    def anonymous(self):
-        """Return unassigned carts."""
-        return self.filter(user=None)
-
-    def open(self):
-        """Return `OPEN` carts."""
-        return self.filter(status=CartStatus.OPEN)
-
-    def canceled(self):
-        """Return `CANCELED` carts."""
-        return self.filter(status=CartStatus.CANCELED)
-
-    def for_display(self):
-        """Annotate the queryset for display purposes.
-
-        Prefetches additional data from the database to avoid the n+1 queries
-        problem.
-        """
-        return self.prefetch_related(
-            'lines__variant__product__category',
-            'lines__variant__product__images',
-            'lines__variant__product__product_type__product_attributes__values')  # noqa
-
-
-class Cart(models.Model):
-    """A shopping cart."""
-
-    status = models.CharField(
-        max_length=32, choices=CartStatus.CHOICES, default=CartStatus.OPEN)
-    created = models.DateTimeField(auto_now_add=True)
-    last_status_change = models.DateTimeField(auto_now_add=True)
-    user = models.ForeignKey(
-        settings.AUTH_USER_MODEL, blank=True, null=True, related_name='carts',
-        on_delete=models.CASCADE)
-    email = models.EmailField(blank=True, null=True)
-    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
-    voucher = models.ForeignKey(
-        'discount.Voucher', null=True, related_name='+',
-        on_delete=models.SET_NULL)
-    checkout_data = JSONField(null=True, editable=False)
-    total = MoneyField(
-        currency=settings.DEFAULT_CURRENCY, max_digits=12,
-        decimal_places=settings.DEFAULT_DECIMAL_PLACES, default=0)
-    quantity = models.PositiveIntegerField(default=0)
-
-    objects = CartQueryset.as_manager()
-
-    class Meta:
-        ordering = ('-last_status_change',)
-
-    def update_quantity(self):
-        """Recalculate cart quantity based on lines."""
-        total_lines = self.count()['total_quantity']
-        if not total_lines:
-            total_lines = 0
-        self.quantity = total_lines
-        self.save(update_fields=['quantity'])
-
-    def change_status(self, status):
-        """Change cart status."""
-        # FIXME: investigate replacing with django-fsm transitions
-        if status not in dict(CartStatus.CHOICES):
-            raise ValueError('Not expected status')
-        if status != self.status:
-            self.status = status
-            self.last_status_change = now()
-            self.save()
-
-    def change_user(self, user):
-        """Assign cart to a user.
-
-        If the user already has an open cart assigned, cancel it.
-        """
-        open_cart = find_open_cart_for_user(user)
-        if open_cart is not None:
-            open_cart.change_status(status=CartStatus.CANCELED)
-        self.user = user
-        self.save(update_fields=['user'])
-
-    def is_shipping_required(self):
-        """Return `True` if any of the lines requires shipping."""
-        return any(line.is_shipping_required() for line in self.lines.all())
-
-    def __repr__(self):
-        return 'Cart(quantity=%s)' % (self.quantity,)
-
-    def __len__(self):
-        return self.lines.count()
-
-    def get_total(self, discounts=None, taxes=None):
-        """Return the total cost of the cart prior to shipping."""
-        subtotals = [
-            line.get_total(discounts, taxes) for line in self.lines.all()]
-        if not subtotals:
-            raise AttributeError('Calling get_total() on an empty cart')
-        return sum_prices(subtotals)
-
-    def count(self):
-        """Return the total quantity in cart."""
-        lines = self.lines.all()
-        return lines.aggregate(total_quantity=models.Sum('quantity'))
-
-    def clear(self):
-        """Remove the cart."""
-        self.delete()
-
-    def create_line(self, variant, quantity, data):
-        """Create a cart line for given variant, quantity and optional data.
-
-        The `data` parameter may be used to differentiate between items with
-        different customization options.
-        """
-        return self.lines.create(
-            variant=variant, quantity=quantity, data=data or {})
-
-    def get_line(self, variant, data=None):
-        """Return a line matching the given variant and data if any."""
-        all_lines = self.lines.all()
-        if data is None:
-            data = {}
-        line = [
-            line for line in all_lines
-            if line.variant_id == variant.id and line.data == data]
-        if line:
-            return line[0]
-        return None
-
-    def add(self, variant, quantity=1, data=None, replace=False,
-            check_quantity=True):
-        """Add a product vartiant to cart.
-
-        The `data` parameter may be used to differentiate between items with
-        different customization options.
-
-        If `replace` is truthy then any previous quantity is discarded instead
-        of added to.
-        """
-        cart_line, dummy_created = self.lines.get_or_create(
-            variant=variant, defaults={'quantity': 0, 'data': data or {}})
-        if replace:
-            new_quantity = quantity
-        else:
-            new_quantity = cart_line.quantity + quantity
-
-        if new_quantity < 0:
-            raise ValueError('%r is not a valid quantity (results in %r)' % (
-                quantity, new_quantity))
-
-        if check_quantity:
-            variant.check_quantity(new_quantity)
-
-        cart_line.quantity = new_quantity
-
-        if not cart_line.quantity:
-            cart_line.delete()
-        else:
-            cart_line.save(update_fields=['quantity'])
-        self.update_quantity()
-
-
-class CartLine(models.Model):
-    """A single cart line.
-
-    Multiple lines in the same cart can refer to the same product variant if
-    their `data` field is different.
-    """
-
-    cart = models.ForeignKey(
-        Cart, related_name='lines', on_delete=models.CASCADE)
-    variant = models.ForeignKey(
-        'product.ProductVariant', related_name='+', on_delete=models.CASCADE)
-    quantity = models.PositiveIntegerField(
-        validators=[MinValueValidator(0), MaxValueValidator(999)])
-    data = JSONField(blank=True, default={})
-
-    class Meta:
-        unique_together = ('cart', 'variant', 'data')
-
-    def __str__(self):
-        return smart_str(self.variant)
-
-    def __eq__(self, other):
-        if not isinstance(other, CartLine):
-            return NotImplemented
-
-        return (
-            self.variant == other.variant and
-            self.quantity == other.quantity and
-            self.data == other.data)
-
-    def __ne__(self, other):
-        return not self == other  # pragma: no cover
-
-    def __repr__(self):
-        return 'CartLine(variant=%r, quantity=%r, data=%r)' % (
-            self.variant, self.quantity, self.data)
-
-    def __getstate__(self):
-        return self.variant, self.quantity, self.data
-
-    def __setstate__(self, data):
-        self.variant, self.quantity, self.data = data
-
-    def get_total(self, discounts=None, taxes=None):
-        """Return the total price of this line."""
-        amount = self.quantity * self.variant.get_price(discounts, taxes)
-        return amount.quantize(CENTS)
-
-    def is_shipping_required(self):
-        """Return `True` if the related product variant requires shipping."""
-        return self.variant.is_shipping_required()
diff --git a/saleor/cart/urls.py b/saleor/cart/urls.py
deleted file mode 100644
index 24162846035..00000000000
--- a/saleor/cart/urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.conf.urls import url
-
-from . import views
-
-urlpatterns = [
-    url(r'^$', views.index, name='index'),
-    url(r'^update/(?P<variant_id>\d+)/$', views.update, name='update-line'),
-    url(r'^summary/$', views.summary, name='cart-summary'),
-    url(r'^shipping-options/$', views.get_shipping_options,
-        name='shipping-options')]
diff --git a/saleor/cart/utils.py b/saleor/cart/utils.py
deleted file mode 100644
index 9bf96b64116..00000000000
--- a/saleor/cart/utils.py
+++ /dev/null
@@ -1,233 +0,0 @@
-"""Cart-related utility functions."""
-from datetime import timedelta
-from functools import wraps
-from uuid import UUID
-
-from django.contrib import messages
-from django.db import transaction
-from django.utils.timezone import now
-from django.utils.translation import pgettext_lazy
-from prices import TaxedMoneyRange
-
-from . import CartStatus
-from ..core.exceptions import InsufficientStock
-from ..core.utils import to_local_currency
-from .models import Cart
-
-COOKIE_NAME = 'cart'
-
-
-def set_cart_cookie(simple_cart, response):
-    """Update respons with a cart token cookie."""
-    # FIXME: document why session is not used
-    ten_years = timedelta(days=(365 * 10))
-    response.set_signed_cookie(
-        COOKIE_NAME, simple_cart.token, max_age=int(ten_years.total_seconds()))
-
-
-def contains_unavailable_variants(cart):
-    """Return `True` if cart contains any unfulfillable lines."""
-    try:
-        for line in cart.lines.all():
-            line.variant.check_quantity(line.quantity)
-    except InsufficientStock:
-        return True
-    return False
-
-
-def token_is_valid(token):
-    """Validate a cart token."""
-    if token is None:
-        return False
-    if isinstance(token, UUID):
-        return True
-    try:
-        UUID(token)
-    except ValueError:
-        return False
-    return True
-
-
-def remove_unavailable_variants(cart):
-    """Remove any unavailable items from cart."""
-    for line in cart.lines.all():
-        try:
-            cart.add(line.variant, quantity=line.quantity, replace=True)
-        except InsufficientStock as e:
-            quantity = e.item.quantity_available
-            cart.add(line.variant, quantity=quantity, replace=True)
-
-
-def get_product_variants_and_prices(cart, product):
-    """Get variants and unit prices from cart lines matching the product."""
-    lines = (
-        cart_line for cart_line in cart.lines.all()
-        if cart_line.variant.product_id == product.id)
-    for line in lines:
-        for dummy_i in range(line.quantity):
-            yield line.variant, line.variant.get_price()
-
-
-def get_category_variants_and_prices(cart, root_category):
-    """Get variants and unit prices from cart lines matching the category.
-
-    Product is assumed to be in the category if it belongs to any of its
-    descendant subcategories.
-    """
-    products = {cart_line.variant.product for cart_line in cart.lines.all()}
-    matching_products = set()
-    for product in products:
-        if product.category.is_descendant_of(root_category, include_self=True):
-            matching_products.add(product)
-    for product in matching_products:
-        for line in get_product_variants_and_prices(cart, product):
-            yield line
-
-
-def check_product_availability_and_warn(request, cart):
-    """Warn if cart contains any lines that cannot be fulfilled."""
-    if contains_unavailable_variants(cart):
-        msg = pgettext_lazy(
-            'Cart warning message',
-            'Sorry. We don\'t have that many items in stock. '
-            'Quantity was set to maximum available for now.')
-        messages.warning(request, msg)
-        remove_unavailable_variants(cart)
-
-
-def find_and_assign_anonymous_cart(queryset=Cart.objects.all()):
-    """Assign cart from cookie to request user."""
-    def get_cart(view):
-        @wraps(view)
-        def func(request, *args, **kwargs):
-            response = view(request, *args, **kwargs)
-            token = request.get_signed_cookie(COOKIE_NAME, default=None)
-            if not token_is_valid(token):
-                return response
-            cart = get_anonymous_cart_from_token(
-                token=token, cart_queryset=queryset)
-            if cart is None:
-                return response
-            if request.user.is_authenticated:
-                with transaction.atomic():
-                    cart.change_user(request.user)
-                    carts_to_close = Cart.objects.open().filter(
-                        user=request.user)
-                    carts_to_close = carts_to_close.exclude(token=token)
-                    carts_to_close.update(
-                        status=CartStatus.CANCELED, last_status_change=now())
-                response.delete_cookie(COOKIE_NAME)
-            return response
-
-        return func
-    return get_cart
-
-
-def get_or_create_anonymous_cart_from_token(
-        token, cart_queryset=Cart.objects.all()):
-    """Return an open unassigned cart with given token or create a new one."""
-    return cart_queryset.open().filter(token=token, user=None).get_or_create(
-        defaults={'user': None})[0]
-
-
-def get_or_create_user_cart(user, cart_queryset=Cart.objects.all()):
-    """Return an open cart for given user or create a new one."""
-    return cart_queryset.open().get_or_create(user=user)[0]
-
-
-def get_anonymous_cart_from_token(token, cart_queryset=Cart.objects.all()):
-    """Return an open unassigned cart with given token if any."""
-    return cart_queryset.open().filter(token=token, user=None).first()
-
-
-def get_user_cart(user, cart_queryset=Cart.objects.all()):
-    """Return an open cart for given user if any."""
-    return cart_queryset.open().filter(user=user).first()
-
-
-def get_or_create_cart_from_request(request, cart_queryset=Cart.objects.all()):
-    """Fetch cart from database or create a new one based on cookie."""
-    if request.user.is_authenticated:
-        return get_or_create_user_cart(request.user, cart_queryset)
-    token = request.get_signed_cookie(COOKIE_NAME, default=None)
-    return get_or_create_anonymous_cart_from_token(token, cart_queryset)
-
-
-def get_cart_from_request(request, cart_queryset=Cart.objects.all()):
-    """Fetch cart from database or return a new instance based on cookie."""
-    if request.user.is_authenticated:
-        cart = get_user_cart(request.user, cart_queryset)
-        user = request.user
-    else:
-        token = request.get_signed_cookie(COOKIE_NAME, default=None)
-        cart = get_anonymous_cart_from_token(token, cart_queryset)
-        user = None
-    if cart is not None:
-        return cart
-    return Cart(user=user)
-
-
-def get_or_create_db_cart(cart_queryset=Cart.objects.all()):
-    """Decorate view to always receive a saved cart instance.
-
-    Changes the view signature from `fund(request, ...)` to
-    `func(request, cart, ...)`.
-
-    If no matching cart is found, one will be created and a cookie will be set
-    for users who are not logged in.
-    """
-    # FIXME: behave like middleware and assign cart to request instead
-    def get_cart(view):
-        @wraps(view)
-        def func(request, *args, **kwargs):
-            cart = get_or_create_cart_from_request(request, cart_queryset)
-            response = view(request, cart, *args, **kwargs)
-            if not request.user.is_authenticated:
-                set_cart_cookie(cart, response)
-            return response
-        return func
-    return get_cart
-
-
-def get_or_empty_db_cart(cart_queryset=Cart.objects.all()):
-    """Decorate view to receive a cart if one exists.
-
-    Changes the view signature from `fund(request, ...)` to
-    `func(request, cart, ...)`.
-
-    If no matching cart is found, an unsaved `Cart` instance will be used.
-    """
-    # FIXME: behave like middleware and assign cart to request instead
-    def get_cart(view):
-        @wraps(view)
-        def func(request, *args, **kwargs):
-            cart = get_cart_from_request(request, cart_queryset)
-            return view(request, cart, *args, **kwargs)
-        return func
-    return get_cart
-
-
-def get_cart_data(cart, shipping_range, currency, discounts, taxes):
-    """Return a JSON-serializable representation of the cart."""
-    cart_total = None
-    local_cart_total = None
-    shipping_required = False
-    total_with_shipping = None
-    local_total_with_shipping = None
-    if cart:
-        cart_total = cart.get_total(discounts, taxes)
-        local_cart_total = to_local_currency(cart_total, currency)
-        shipping_required = cart.is_shipping_required()
-        total_with_shipping = TaxedMoneyRange(
-            start=cart_total, stop=cart_total)
-        if shipping_required and shipping_range:
-            total_with_shipping = shipping_range + cart_total
-        local_total_with_shipping = to_local_currency(
-            total_with_shipping, currency)
-
-    return {
-        'cart_total': cart_total,
-        'local_cart_total': local_cart_total,
-        'shipping_required': shipping_required,
-        'total_with_shipping': total_with_shipping,
-        'local_total_with_shipping': local_total_with_shipping}
diff --git a/saleor/cart/views.py b/saleor/cart/views.py
deleted file mode 100644
index 1ff3bea6f45..00000000000
--- a/saleor/cart/views.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""Cart-related views."""
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404, redirect, render
-from django.template.response import TemplateResponse
-
-from ..core.utils import (
-    format_money, get_user_shipping_country, to_local_currency)
-from ..product.models import ProductVariant
-from ..shipping.utils import get_shipment_options
-from .forms import CountryForm, ReplaceCartLineForm
-from .models import Cart
-from .utils import (
-    check_product_availability_and_warn, get_cart_data, get_or_empty_db_cart)
-
-
-@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
-def index(request, cart):
-    """Display cart details."""
-    discounts = request.discounts
-    taxes = request.taxes
-    cart_lines = []
-    check_product_availability_and_warn(request, cart)
-
-    # refresh required to get updated cart lines and it's quantity
-    try:
-        cart = Cart.objects.prefetch_related(
-            'lines__variant__product__category').get(pk=cart.pk)
-    except Cart.DoesNotExist:
-        pass
-
-    lines = cart.lines.select_related(
-        'variant__product__product_type',
-        'variant__product__category')
-    lines = lines.prefetch_related(
-        'variant__product__images',
-        'variant__product__product_type__variant_attributes')
-    for line in lines:
-        initial = {'quantity': line.quantity}
-        form = ReplaceCartLineForm(
-            None, cart=cart, variant=line.variant, initial=initial,
-            discounts=discounts, taxes=taxes)
-        cart_lines.append({
-            'variant': line.variant,
-            'get_price': line.variant.get_price(discounts, taxes),
-            'get_total': line.get_total(discounts, taxes),
-            'form': form})
-
-    default_country = get_user_shipping_country(request)
-    country_form = CountryForm(initial={'country': default_country})
-    default_country_options = get_shipment_options(default_country, taxes)
-
-    cart_data = get_cart_data(
-        cart, default_country_options, request.currency, discounts, taxes)
-    ctx = {
-        'cart_lines': cart_lines,
-        'country_form': country_form,
-        'default_country_options': default_country_options}
-    ctx.update(cart_data)
-
-    return TemplateResponse(
-        request, 'cart/index.html', ctx)
-
-
-@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
-def get_shipping_options(request, cart):
-    """Display shipping options to get a price estimate."""
-    country_form = CountryForm(request.POST or None, taxes=request.taxes)
-    if country_form.is_valid():
-        shipments = country_form.get_shipment_options()
-    else:
-        shipments = None
-    ctx = {
-        'default_country_options': shipments,
-        'country_form': country_form}
-    cart_data = get_cart_data(
-        cart, shipments, request.currency, request.discounts, request.taxes)
-    ctx.update(cart_data)
-    return TemplateResponse(
-        request, 'cart/_subtotal_table.html', ctx)
-
-
-@get_or_empty_db_cart()
-def update(request, cart, variant_id):
-    """Update the line quantities."""
-    if not request.is_ajax():
-        return redirect('cart:index')
-    variant = get_object_or_404(ProductVariant, pk=variant_id)
-    discounts = request.discounts
-    taxes = request.taxes
-    status = None
-    form = ReplaceCartLineForm(
-        request.POST, cart=cart, variant=variant, discounts=discounts,
-        taxes=taxes)
-    if form.is_valid():
-        form.save()
-        response = {
-            'variantId': variant_id,
-            'subtotal': 0,
-            'total': 0,
-            'cart': {
-                'numItems': cart.quantity,
-                'numLines': len(cart)}}
-        updated_line = cart.get_line(form.cart_line.variant)
-        if updated_line:
-            response['subtotal'] = format_money(
-                updated_line.get_total(discounts, taxes).gross)
-        if cart:
-            cart_total = cart.get_total(discounts, taxes)
-            response['total'] = format_money(cart_total.gross)
-            local_cart_total = to_local_currency(cart_total, request.currency)
-            if local_cart_total is not None:
-                response['localTotal'] = format_money(local_cart_total.gross)
-        status = 200
-    elif request.POST is not None:
-        response = {'error': form.errors}
-        status = 400
-    return JsonResponse(response, status=status)
-
-
-@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
-def summary(request, cart):
-    """Display a cart summary suitable for displaying on all pages."""
-    discounts = request.discounts
-    taxes = request.taxes
-
-    def prepare_line_data(line):
-        first_image = line.variant.get_first_image()
-        return {
-            'product': line.variant.product,
-            'variant': line.variant.name,
-            'quantity': line.quantity,
-            'image': first_image,
-            'line_total': line.get_total(discounts, taxes),
-            'variant_url': line.variant.get_absolute_url()}
-
-    if cart.quantity == 0:
-        data = {'quantity': 0}
-    else:
-        data = {
-            'quantity': cart.quantity,
-            'total': cart.get_total(discounts, taxes),
-            'lines': [prepare_line_data(line) for line in cart.lines.all()]}
-
-    return render(request, 'cart_dropdown.html', data)
diff --git a/saleor/checkout/__init__.py b/saleor/checkout/__init__.py
index e69de29bb2d..c650761f774 100644
--- a/saleor/checkout/__init__.py
+++ b/saleor/checkout/__init__.py
@@ -0,0 +1,20 @@
+import logging
+
+from django.utils.translation import pgettext_lazy
+
+logger = logging.getLogger(__name__)
+
+
+class AddressType:
+    BILLING = 'billing'
+    SHIPPING = 'shipping'
+
+    CHOICES = [
+        (BILLING, pgettext_lazy(
+            'Type of address used to fulfill order',
+            'Billing'
+        )),
+        (SHIPPING, pgettext_lazy(
+            'Type of address used to fulfill order',
+            'Shipping'
+        ))]
diff --git a/saleor/cart/context_processors.py b/saleor/checkout/context_processors.py
similarity index 100%
rename from saleor/cart/context_processors.py
rename to saleor/checkout/context_processors.py
diff --git a/saleor/checkout/core.py b/saleor/checkout/core.py
deleted file mode 100644
index 9b47ce6795b..00000000000
--- a/saleor/checkout/core.py
+++ /dev/null
@@ -1,430 +0,0 @@
-"""Checkout session state management."""
-from datetime import date
-from functools import wraps
-
-from django.db import transaction
-from django.forms.models import model_to_dict
-from django.utils.encoding import smart_text
-from django.utils.translation import get_language
-from prices import Money
-
-from ..account.models import Address
-from ..account.utils import store_user_address
-from ..cart.models import Cart
-from ..cart.utils import get_or_empty_db_cart
-from ..core import analytics
-from ..core.utils.taxes import ZERO_TAXED_MONEY, get_taxes_for_country
-from ..discount.models import NotApplicable, Voucher
-from ..discount.utils import increase_voucher_usage
-from ..order.models import Order
-from ..order.utils import add_variant_to_order
-from ..shipping.models import ANY_COUNTRY, ShippingMethodCountry
-from .utils import get_voucher_discount_for_checkout
-
-STORAGE_SESSION_KEY = 'checkout_storage'
-
-
-class Checkout:
-    """Represents a checkout session.
-
-    This object acts a temporary storage for the entire checkout session. An
-    order instance is only created when the user confirms their order and is
-    ready to pay.
-
-    `VERSION` is used to prevent code trying to work with incompatible
-    checkout structures.
-
-    The `modified` attribute keeps track of when checkout state changes and
-    needs to be saved.
-    """
-
-    VERSION = '1.0.0'
-
-    def __init__(self, cart, user, discounts, taxes, tracking_code):
-        self.modified = False
-        self.cart = cart
-        self.user = user
-        self.discounts = discounts
-        self.taxes = taxes
-        self.tracking_code = tracking_code
-        self.storage = {'version': self.VERSION}
-        self._shipping_method = None
-        self._shipping_address = None
-
-    @classmethod
-    def from_storage(
-            cls, storage_data, cart, user, discounts, taxes, tracking_code):
-        """Restore a previously serialized checkout session.
-
-        `storage_data` is the value previously returned by
-        `Checkout.for_storage()`.
-        """
-        checkout = cls(cart, user, discounts, taxes, tracking_code)
-        checkout.storage = storage_data
-        try:
-            version = checkout.storage['version']
-        except (TypeError, KeyError):
-            version = None
-        if version != cls.VERSION:
-            checkout.storage = {'version': cls.VERSION}
-        return checkout
-
-    def for_storage(self):
-        """Serialize a checkout session to allow persistence.
-
-        The session can later be restored using `Checkout.from_storage()`.
-        """
-        return self.storage
-
-    def clear_storage(self):
-        """Discard the entire state."""
-        self.storage = None
-        self.modified = True
-
-    def _get_address_from_storage(self, key):
-        address_data = self.storage.get(key)
-        if address_data is not None and address_data.get('id'):
-            try:
-                return Address.objects.get(id=address_data['id'])
-            except Address.DoesNotExist:
-                return None
-        elif address_data:
-            return Address(**address_data)
-        return None
-
-    @property
-    def lines(self):
-        """Return the cart lines data."""
-        for line in self.cart.lines.all():
-            line_total = line.get_total(self.discounts, self.get_taxes())
-            yield line, line_total
-
-    @property
-    def is_shipping_required(self):
-        """Return `True` if this checkout session needs shipping."""
-        return self.cart.is_shipping_required()
-
-    @property
-    def are_taxes_handled(self):
-        """Return `True` if taxes are handled in the delivery country."""
-        return bool(self.get_taxes())
-
-    @property
-    def shipping_address(self):
-        """Return a shipping address if any."""
-        if self._shipping_address is None:
-            address = self._get_address_from_storage('shipping_address')
-            if address is None and self.user.is_authenticated:
-                address = self.user.default_shipping_address
-            self._shipping_address = address
-        return self._shipping_address
-
-    @shipping_address.setter
-    def shipping_address(self, address):
-        address_data = model_to_dict(address)
-        phone_number = address_data.get('phone')
-        if phone_number:
-            address_data['phone'] = str(address_data['phone'])
-        address_data['country'] = smart_text(address_data['country'])
-        self.storage['shipping_address'] = address_data
-        self.modified = True
-        self._shipping_address = address
-
-    @property
-    def shipping_method(self):
-        """Return a shipping method if any."""
-        if self._shipping_method is None:
-            shipping_address = self.shipping_address
-            if shipping_address is None:
-                return None
-            shipping_method_country_id = self.storage.get(
-                'shipping_method_country_id')
-            if shipping_method_country_id is None:
-                return None
-            try:
-                shipping_method_country = ShippingMethodCountry.objects.get(
-                    id=shipping_method_country_id)
-            except ShippingMethodCountry.DoesNotExist:
-                return None
-            shipping_country_code = shipping_address.country.code
-            allowed_codes = [ANY_COUNTRY, shipping_country_code]
-            if shipping_method_country.country_code not in allowed_codes:
-                return None
-            self._shipping_method = shipping_method_country
-        return self._shipping_method
-
-    @shipping_method.setter
-    def shipping_method(self, shipping_method_country):
-        self.storage['shipping_method_country_id'] = shipping_method_country.id
-        self.modified = True
-        self._shipping_method = shipping_method_country
-
-    @property
-    def shipping_price(self):
-        shipping_method = self.shipping_method
-        return (
-            shipping_method.get_total_price(self.get_taxes())
-            if shipping_method else ZERO_TAXED_MONEY)
-
-    @property
-    def email(self):
-        """Return the customer email if any."""
-        return self.storage.get('email')
-
-    @email.setter
-    def email(self, email):
-        self.storage['email'] = email
-        self.modified = True
-
-    @property
-    def note(self):
-        return self.storage.get('note')
-
-    @note.setter
-    def note(self, note):
-        self.storage['note'] = note
-        self.modified = True
-
-    @property
-    def billing_address(self):
-        """Return the billing addres if any."""
-        address = self._get_address_from_storage('billing_address')
-        if address is not None:
-            return address
-        elif (self.user.is_authenticated and
-              self.user.default_billing_address):
-            return self.user.default_billing_address
-        elif self.shipping_address:
-            return self.shipping_address
-        return None
-
-    @billing_address.setter
-    def billing_address(self, address):
-        address_data = model_to_dict(address)
-        address_data['country'] = smart_text(address_data['country'])
-        self.storage['billing_address'] = address_data
-        self.modified = True
-
-    @property
-    def discount(self):
-        """Return a discount if any."""
-        value = self.storage.get('discount_value')
-        currency = self.storage.get('discount_currency')
-        if value is not None and currency is not None:
-            return Money(value, currency)
-        return None
-
-    @discount.setter
-    def discount(self, discount):
-        self.storage['discount_value'] = smart_text(discount.amount)
-        self.storage['discount_currency'] = discount.currency
-        self.modified = True
-
-    @discount.deleter
-    def discount(self):
-        if 'discount_value' in self.storage:
-            del self.storage['discount_value']
-            self.modified = True
-        if 'discount_currency' in self.storage:
-            del self.storage['discount_currency']
-            self.modified = True
-
-    @property
-    def discount_name(self):
-        """Return a discount name if any."""
-        return self.storage.get('discount_name')
-
-    @discount_name.setter
-    def discount_name(self, discount_name):
-        self.storage['discount_name'] = discount_name
-        self.modified = True
-
-    @discount_name.deleter
-    def discount_name(self):
-        if 'discount_name' in self.storage:
-            del self.storage['discount_name']
-            self.modified = True
-
-    @property
-    def voucher_code(self):
-        """Return a discount voucher code if any."""
-        return self.storage.get('voucher_code')
-
-    @voucher_code.setter
-    def voucher_code(self, voucher_code):
-        self.storage['voucher_code'] = voucher_code
-        self.modified = True
-
-    @voucher_code.deleter
-    def voucher_code(self):
-        if 'voucher_code' in self.storage:
-            del self.storage['voucher_code']
-            self.modified = True
-
-    @property
-    def is_shipping_same_as_billing(self):
-        """Return `True` if shipping and billing addresses are identical."""
-        return self.shipping_address == self.billing_address
-
-    def _add_to_user_address_book(self, address, is_billing=False,
-                                  is_shipping=False):
-        if self.user.is_authenticated:
-            store_user_address(
-                self.user, address, shipping=is_shipping,
-                billing=is_billing)
-
-    def _save_order_billing_address(self):
-        return self.billing_address.get_copy()
-
-    def _save_order_shipping_address(self):
-        return self.shipping_address.get_copy()
-
-    @transaction.atomic
-    def create_order(self):
-        """Create an order from the checkout session.
-
-        Each order will get a private copy of both the billing and the shipping
-        address (if shipping ).
-
-        If any of the addresses is new and the user is logged in the address
-        will also get saved to that user's address book.
-
-        Current user's language is saved in the order so we can later determine
-        which language to use when sending email.
-        """
-        # FIXME: save locale along with the language
-        voucher = self._get_voucher(
-            vouchers=Voucher.objects.active(date=date.today())
-            .select_for_update())
-        if self.voucher_code is not None and voucher is None:
-            # Voucher expired in meantime, abort order placement
-            return None
-
-        if self.is_shipping_required:
-            shipping_address = self._save_order_shipping_address()
-            self._add_to_user_address_book(
-                self.shipping_address, is_shipping=True)
-        else:
-            shipping_address = None
-        billing_address = self._save_order_billing_address()
-        self._add_to_user_address_book(
-            self.billing_address, is_billing=True)
-
-        shipping_method_name = (
-            smart_text(self.shipping_method) if self.is_shipping_required
-            else None)
-        order_data = {
-            'language_code': get_language(),
-            'billing_address': billing_address,
-            'shipping_address': shipping_address,
-            'tracking_client_id': self.tracking_code,
-            'shipping_price': self.shipping_price,
-            'shipping_method_name': shipping_method_name,
-            'total': self.get_total()}
-
-        if self.user.is_authenticated:
-            order_data['user'] = self.user
-            order_data['user_email'] = self.user.email
-        else:
-            order_data['user_email'] = self.email
-
-        if voucher is not None:
-            order_data['voucher'] = voucher
-            order_data['discount_amount'] = self.discount
-            order_data['discount_name'] = self.discount_name
-
-        order = Order.objects.create(**order_data)
-
-        for line in self.cart.lines.all():
-            add_variant_to_order(
-                order, line.variant, line.quantity, self.discounts,
-                self.get_taxes(), add_to_existing=False)
-
-        if voucher is not None:
-            increase_voucher_usage(voucher)
-
-        if self.note is not None and self.note:
-            order.notes.create(user=order.user, content=self.note)
-
-        return order
-
-    def _get_voucher(self, vouchers=None):
-        voucher_code = self.voucher_code
-        if voucher_code is not None:
-            if vouchers is None:
-                vouchers = Voucher.objects.active(date=date.today())
-            try:
-                return vouchers.get(code=self.voucher_code)
-            except Voucher.DoesNotExist:
-                return None
-        return None
-
-    def recalculate_discount(self):
-        """Recalculate `self.discount` based on the voucher.
-
-        Will clear both voucher and discount if the discount is no longer
-        applicable.
-        """
-        voucher = self._get_voucher()
-        if voucher is not None:
-            try:
-                self.discount = get_voucher_discount_for_checkout(
-                    voucher, self)
-                self.discount_name = voucher.name
-            except NotApplicable:
-                del self.discount
-                del self.discount_name
-                del self.voucher_code
-        else:
-            del self.discount
-            del self.discount_name
-            del self.voucher_code
-
-    def get_subtotal(self):
-        """Calculate order total without shipping and discount."""
-        return self.cart.get_total(self.discounts, self.get_taxes())
-
-    def get_total(self):
-        """Calculate order total with shipping and discount amount."""
-        total = self.get_subtotal()
-        if self.shipping_method and self.is_shipping_required:
-            total += self.shipping_price
-        if self.discount:
-            total -= self.discount
-        return total
-
-    def get_taxes(self):
-        """Return taxes based on shipping address (if set) or IP country."""
-        shipping_address = self.shipping_address
-
-        if shipping_address:
-            return get_taxes_for_country(shipping_address.country)
-
-        return self.taxes
-
-
-def load_checkout(view):
-    """Decorate view with checkout session and cart for each request.
-
-    Any views decorated by this will change their signature from
-    `func(request)` to `func(request, checkout, cart)`.
-    """
-    # FIXME: behave like middleware and assign checkout and cart to request
-    # instead of changing the view signature
-    @wraps(view)
-    @get_or_empty_db_cart(Cart.objects.for_display())
-    def func(request, cart):
-        try:
-            session_data = request.session[STORAGE_SESSION_KEY]
-        except KeyError:
-            session_data = ''
-        tracking_code = analytics.get_client_id(request)
-        checkout = Checkout.from_storage(
-            session_data, cart, request.user, request.discounts, request.taxes,
-            tracking_code)
-        response = view(request, checkout, cart)
-        if checkout.modified:
-            request.session[STORAGE_SESSION_KEY] = checkout.for_storage()
-        return response
-
-    return func
diff --git a/saleor/checkout/forms.py b/saleor/checkout/forms.py
index 479f0cf8a71..5813d121799 100644
--- a/saleor/checkout/forms.py
+++ b/saleor/checkout/forms.py
@@ -1,43 +1,225 @@
-"""Checkout-related forms."""
+"""Cart-related forms and fields."""
+from datetime import date
+
 from django import forms
+from django.conf import settings
+from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist
+from django.utils.encoding import smart_text
 from django.utils.safestring import mark_safe
-from django.utils.translation import pgettext_lazy
+from django.utils.translation import npgettext_lazy, pgettext_lazy
+from django_countries.fields import LazyTypedChoiceField, countries
 
+from ..core.exceptions import InsufficientStock
 from ..core.utils import format_money
 from ..core.utils.taxes import display_gross_prices
+from ..discount.models import NotApplicable, Voucher
 from ..shipping.models import ShippingMethodCountry
-from ..shipping.utils import get_taxed_shipping_price
+from ..shipping.utils import get_shipment_options, get_taxed_shipping_price
+from .models import Cart
 
 
-class CheckoutAddressField(forms.ChoiceField):
-    """Like a choice field but uses a radio group instead of a dropdown."""
+class QuantityField(forms.IntegerField):
+    """A specialized integer field with initial quantity and min/max values."""
 
-    widget = forms.RadioSelect()
+    def __init__(self, **kwargs):
+        super().__init__(
+            min_value=0, max_value=settings.MAX_CART_LINE_QUANTITY,
+            initial=1, **kwargs)
+
+
+class AddToCartForm(forms.Form):
+    """Add-to-cart form.
+
+    Allows selection of a product variant and quantity.
+
+    The save method adds it to the cart.
+    """
 
+    quantity = QuantityField(
+        label=pgettext_lazy('Add to cart form field label', 'Quantity'))
+    error_messages = {
+        'not-available': pgettext_lazy(
+            'Add to cart form error',
+            'Sorry. This product is currently not available.'),
+        'empty-stock': pgettext_lazy(
+            'Add to cart form error',
+            'Sorry. This product is currently out of stock.'),
+        'variant-does-not-exists': pgettext_lazy(
+            'Add to cart form error',
+            'Oops. We could not find that product.'),
+        'insufficient-stock': npgettext_lazy(
+            'Add to cart form error',
+            'Only %d remaining in stock.',
+            'Only %d remaining in stock.')}
 
-class ShippingAddressesForm(forms.Form):
-    """Shipping address form."""
+    def __init__(self, *args, **kwargs):
+        self.cart = kwargs.pop('cart')
+        self.product = kwargs.pop('product')
+        self.discounts = kwargs.pop('discounts', ())
+        self.taxes = kwargs.pop('taxes', {})
+        super().__init__(*args, **kwargs)
+
+    def clean(self):
+        """Clean the form.
+
+        Makes sure the total quantity in cart (taking into account what was
+        already there) does not exceed available quantity.
+        """
+        cleaned_data = super().clean()
+        quantity = cleaned_data.get('quantity')
+        if quantity is None:
+            return cleaned_data
+        try:
+            variant = self.get_variant(cleaned_data)
+        except ObjectDoesNotExist:
+            msg = self.error_messages['variant-does-not-exists']
+            self.add_error(NON_FIELD_ERRORS, msg)
+        else:
+            line = self.cart.get_line(variant)
+            used_quantity = line.quantity if line else 0
+            new_quantity = quantity + used_quantity
+            try:
+                variant.check_quantity(new_quantity)
+            except InsufficientStock as e:
+                remaining = e.item.quantity_available - used_quantity
+                if remaining:
+                    msg = self.error_messages['insufficient-stock']
+                    self.add_error('quantity', msg % remaining)
+                else:
+                    msg = self.error_messages['empty-stock']
+                    self.add_error('quantity', msg)
+        return cleaned_data
+
+    def save(self):
+        """Add the selected product variant and quantity to the cart."""
+        from .utils import add_variant_to_cart
+        variant = self.get_variant(self.cleaned_data)
+        quantity = self.cleaned_data['quantity']
+        add_variant_to_cart(self.cart, variant, quantity)
+
+    def get_variant(self, cleaned_data):
+        """Return a product variant that matches submitted values.
+
+        This allows specialized implementations to select the variant based on
+        multiple fields (like size and color) instead of having a single
+        variant selection widget.
+        """
+        raise NotImplementedError()
+
+
+class ReplaceCartLineForm(AddToCartForm):
+    """Replace quantity in cart form.
+
+    Similar to AddToCartForm but its save method replaces the quantity.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.variant = kwargs.pop('variant')
+        kwargs['product'] = self.variant.product
+        super().__init__(*args, **kwargs)
+        self.cart_line = self.cart.get_line(self.variant)
+        self.fields['quantity'].widget.attrs = {
+            'min': 1, 'max': settings.MAX_CART_LINE_QUANTITY}
+
+    def clean_quantity(self):
+        """Clean the quantity field.
+
+        Checks if target quantity does not exceed the currently available
+        quantity.
+        """
+        quantity = self.cleaned_data['quantity']
+        try:
+            self.variant.check_quantity(quantity)
+        except InsufficientStock as e:
+            msg = self.error_messages['insufficient-stock']
+            raise forms.ValidationError(
+                msg % e.item.quantity_available)
+        return quantity
+
+    def clean(self):
+        """Clean the form skipping the add-to-form checks."""
+        # explicitly skip parent's implementation
+        # pylint: disable=E1003
+        return super(AddToCartForm, self).clean()
+
+    def get_variant(self, cleaned_data):
+        """Return the matching variant.
+
+        In this case we explicitly know the variant as we're modifying an
+        existing line in cart.
+        """
+        return self.variant
+
+    def save(self):
+        """Replace the selected product's quantity in cart."""
+        from .utils import add_variant_to_cart
+        variant = self.get_variant(self.cleaned_data)
+        quantity = self.cleaned_data['quantity']
+        add_variant_to_cart(self.cart, variant, quantity, replace=True)
+
+
+class CountryForm(forms.Form):
+    """Country selection form."""
+
+    country = LazyTypedChoiceField(
+        label=pgettext_lazy('Country form field label', 'Country'),
+        choices=countries)
+
+    def __init__(self, *args, **kwargs):
+        self.taxes = kwargs.pop('taxes', {})
+        super().__init__(*args, **kwargs)
+
+    def get_shipment_options(self):
+        """Return a list of shipping methods for the selected country."""
+        code = self.cleaned_data['country']
+        return get_shipment_options(code, self.taxes)
+
+
+class AnonymousUserShippingForm(forms.ModelForm):
+    """Additional shipping information form for users who are not logged in."""
+
+    email = forms.EmailField(
+        widget=forms.EmailInput(attrs={'autocomplete': 'shipping email'}),
+        label=pgettext_lazy('Address form field label', 'Email'))
+
+    class Meta:
+        model = Cart
+        fields = ['email']
+
+
+class AnonymousUserBillingForm(forms.ModelForm):
+    """Additional billing information form for users who are not logged in."""
+
+    email = forms.EmailField(
+        widget=forms.EmailInput(attrs={'autocomplete': 'billing email'}),
+        label=pgettext_lazy('Address form field label', 'Email'))
+
+    class Meta:
+        model = Cart
+        fields = ['email']
+
+
+class AddressChoiceForm(forms.Form):
+    """Choose one of user's addresses or to create new one."""
 
     NEW_ADDRESS = 'new_address'
     CHOICES = [
         (NEW_ADDRESS, pgettext_lazy(
             'Shipping addresses form choice', 'Enter a new address'))]
 
-    address = CheckoutAddressField(
+    address = forms.ChoiceField(
         label=pgettext_lazy('Shipping addresses form field label', 'Address'),
-        choices=CHOICES, initial=NEW_ADDRESS)
+        choices=CHOICES, initial=NEW_ADDRESS, widget=forms.RadioSelect)
 
     def __init__(self, *args, **kwargs):
-        additional_addresses = kwargs.pop('additional_addresses', [])
+        addresses = kwargs.pop('addresses')
         super().__init__(*args, **kwargs)
-        address_field = self.fields['address']
-        address_choices = [
-            (address.id, str(address)) for address in additional_addresses]
-        address_field.choices = self.CHOICES + address_choices
+        address_choices = [(address.id, str(address)) for address in addresses]
+        self.fields['address'].choices = self.CHOICES + address_choices
 
 
-class BillingAddressesForm(ShippingAddressesForm):
-    """Billing address form."""
+class BillingAddressChoiceForm(AddressChoiceForm):
+    """Choose one of user's addresses, a shipping one or to create new."""
 
     NEW_ADDRESS = 'new_address'
     SHIPPING_ADDRESS = 'shipping_address'
@@ -47,31 +229,21 @@ class BillingAddressesForm(ShippingAddressesForm):
         (SHIPPING_ADDRESS, pgettext_lazy(
             'Billing addresses form choice', 'Same as shipping'))]
 
-    address = CheckoutAddressField(choices=CHOICES, initial=SHIPPING_ADDRESS)
+    address = forms.ChoiceField(
+        label=pgettext_lazy('Billing addresses form field label', 'Address'),
+        choices=CHOICES, initial=SHIPPING_ADDRESS, widget=forms.RadioSelect)
 
 
-class BillingWithoutShippingAddressForm(ShippingAddressesForm):
-    """Billing address form when shipping is not required.
-
-    Same as the default shipping address as in this came "billing same as
-    shipping" option does not make sense.
-    """
-
-
-# FIXME: why is this called a country choice field?
-class ShippingCountryChoiceField(forms.ModelChoiceField):
-    """Shipping method choice field.
+class ShippingCountryMethodChoiceField(forms.ModelChoiceField):
+    """Shipping method country choice field.
 
     Uses a radio group instead of a dropdown and includes estimated shipping
     prices.
     """
 
+    taxes = None
     widget = forms.RadioSelect()
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.taxes = None
-
     def label_from_instance(self, obj):
         """Return a friendly label for the shipping method."""
         price = get_taxed_shipping_price(obj.price, self.taxes)
@@ -83,63 +255,94 @@ def label_from_instance(self, obj):
         label = mark_safe('%s %s' % (obj.shipping_method, price_html))
         return label
 
-    def set_taxes(self, taxes):
-        self.taxes = taxes
 
+class CartShippingMethodForm(forms.ModelForm):
+    """Cart shipping method form."""
 
-class ShippingMethodForm(forms.Form):
-    """Shipping method form."""
-
-    method = ShippingCountryChoiceField(
+    shipping_method = ShippingCountryMethodChoiceField(
         queryset=ShippingMethodCountry.objects.select_related(
             'shipping_method').order_by('price').all(),
         label=pgettext_lazy(
             'Shipping method form field label', 'Shipping method'),
         required=True)
 
-    def __init__(self, country_code, taxes, *args, **kwargs):
+    class Meta:
+        model = Cart
+        fields = ['shipping_method']
+
+    def __init__(self, *args, **kwargs):
+        taxes = kwargs.pop('taxes')
         super().__init__(*args, **kwargs)
-        method_field = self.fields['method']
-        method_field.set_taxes(taxes)
+        method_field = self.fields['shipping_method']
+        method_field.taxes = taxes
+
+        country_code = self.instance.shipping_address.country.code
         if country_code:
             queryset = method_field.queryset
             method_field.queryset = queryset.unique_for_country_code(
                 country_code)
 
-        if self.initial.get('method') is None:
-            self.initial['method'] = method_field.queryset.first()
+        if self.initial.get('shipping_method') is None:
+            self.initial['shipping_method'] = method_field.queryset.first()
 
         method_field.empty_label = None
 
 
-class AnonymousUserShippingForm(forms.Form):
-    """Additional shipping information form for users who are not logged in."""
+class CartNoteForm(forms.ModelForm):
+    """Save note in cart."""
 
-    email = forms.EmailField(
-        required=True, widget=forms.EmailInput(
-            attrs={'autocomplete': 'shipping email'}),
-        label=pgettext_lazy('Shipping form field label', 'Email'))
+    note = forms.CharField(
+        max_length=250, required=False, strip=True, label=False,
+        widget=forms.Textarea({'rows': 3}))
 
+    class Meta:
+        model = Cart
+        fields = ['note']
 
-class AnonymousUserBillingForm(forms.Form):
-    """Additional billing information form for users who are not logged in."""
 
-    email = forms.EmailField(
-        required=True, widget=forms.EmailInput(
-            attrs={'autocomplete': 'billing email'}),
-        label=pgettext_lazy('Billing form field label', 'Email'))
+class VoucherField(forms.ModelChoiceField):
 
+    default_error_messages = {
+        'invalid_choice': pgettext_lazy(
+            'Voucher form error', 'Discount code incorrect or expired')}
 
-class NoteForm(forms.Form):
-    """Form to add a note to an order."""
 
-    note = forms.CharField(
-        max_length=250, required=False, strip=True, label=False)
-    note.widget = forms.Textarea({'rows': 3})
+class CartVoucherForm(forms.ModelForm):
+    """Apply voucher to a cart form."""
+
+    voucher = VoucherField(
+        queryset=Voucher.objects.none(),
+        to_field_name='code',
+        help_text=pgettext_lazy(
+            'Checkout discount form label for voucher field',
+            'Gift card or discount code'),
+        widget=forms.TextInput)
+
+    class Meta:
+        model = Cart
+        fields = ['voucher']
 
     def __init__(self, *args, **kwargs):
-        self.checkout = kwargs.pop('checkout', None)
         super().__init__(*args, **kwargs)
-
-    def set_checkout_note(self):
-        self.checkout.note = self.cleaned_data.get('note', '')
+        self.fields['voucher'].queryset = Voucher.objects.active(
+            date=date.today())
+
+    def clean(self):
+        from .utils import get_voucher_discount_for_cart
+        cleaned_data = super().clean()
+        if 'voucher' in cleaned_data:
+            voucher = cleaned_data['voucher']
+            try:
+                discount_amount = get_voucher_discount_for_cart(
+                    voucher, self.instance)
+                cleaned_data['discount_amount'] = discount_amount
+            except NotApplicable as e:
+                self.add_error('voucher', smart_text(e))
+        return cleaned_data
+
+    def save(self, commit=True):
+        voucher = self.cleaned_data['voucher']
+        self.instance.voucher_code = voucher.code
+        self.instance.discount_name = voucher.name
+        self.instance.discount_amount = self.cleaned_data['discount_amount']
+        return super().save(commit)
diff --git a/saleor/cart/migrations/0001_auto_20170113_0435.py b/saleor/checkout/migrations/0001_auto_20170113_0435.py
similarity index 83%
rename from saleor/cart/migrations/0001_auto_20170113_0435.py
rename to saleor/checkout/migrations/0001_auto_20170113_0435.py
index cfc9c7688ab..4685670cb44 100644
--- a/saleor/cart/migrations/0001_auto_20170113_0435.py
+++ b/saleor/checkout/migrations/0001_auto_20170113_0435.py
@@ -9,7 +9,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', 'fix_empty_data_in_lines'),
+        ('checkout', 'fix_empty_data_in_lines'),
+    ]
+
+    replaces = [
+        ('cart', '0001_auto_20170113_0435'),
     ]
 
     operations = [
diff --git a/saleor/cart/migrations/0001_initial.py b/saleor/checkout/migrations/0001_initial.py
similarity index 92%
rename from saleor/cart/migrations/0001_initial.py
rename to saleor/checkout/migrations/0001_initial.py
index 74513ad4f67..6d001c57a44 100644
--- a/saleor/cart/migrations/0001_initial.py
+++ b/saleor/checkout/migrations/0001_initial.py
@@ -18,6 +18,10 @@ class Migration(migrations.Migration):
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
+    replaces = [
+        ('cart', '0001_initial'),
+    ]
+
     operations = [
         migrations.CreateModel(
             name='Cart',
@@ -34,6 +38,7 @@ class Migration(migrations.Migration):
                 ('voucher', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to='discount.Voucher', null=True)),
             ],
             options={
+                'db_table': 'cart_cart',
                 'ordering': ('-last_status_change',),
             },
         ),
@@ -43,9 +48,12 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('quantity', models.PositiveIntegerField(verbose_name='quantity', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(999)])),
                 ('data', models.TextField(default='{}', blank=True)),
-                ('cart', models.ForeignKey(related_name='lines', to='cart.Cart', on_delete=django.db.models.deletion.CASCADE)),
+                ('cart', models.ForeignKey(related_name='lines', to='checkout.Cart', on_delete=django.db.models.deletion.CASCADE)),
                 ('product', models.ForeignKey(related_name='+', verbose_name='product', to='product.ProductVariant', on_delete=django.db.models.deletion.CASCADE)),
             ],
+            options={
+                'db_table': 'cart_cartline',
+            },
         ),
         migrations.AlterUniqueTogether(
             name='cartline',
diff --git a/saleor/cart/migrations/0002_auto_20161014_1221.py b/saleor/checkout/migrations/0002_auto_20161014_1221.py
similarity index 84%
rename from saleor/cart/migrations/0002_auto_20161014_1221.py
rename to saleor/checkout/migrations/0002_auto_20161014_1221.py
index edf752fee10..9ed241380ff 100644
--- a/saleor/cart/migrations/0002_auto_20161014_1221.py
+++ b/saleor/checkout/migrations/0002_auto_20161014_1221.py
@@ -9,7 +9,11 @@ class Migration(migrations.Migration):
 
     dependencies = [
         ('product', '0012_auto_20160218_0812'),
-        ('cart', '0001_initial'),
+        ('checkout', '0001_initial'),
+    ]
+
+    replaces = [
+        ('cart', '0002_auto_20161014_1221'),
     ]
 
     operations = [
diff --git a/saleor/cart/migrations/0002_auto_20170206_0407.py b/saleor/checkout/migrations/0002_auto_20170206_0407.py
similarity index 91%
rename from saleor/cart/migrations/0002_auto_20170206_0407.py
rename to saleor/checkout/migrations/0002_auto_20170206_0407.py
index 48e1f9f7804..061c1d29304 100644
--- a/saleor/cart/migrations/0002_auto_20170206_0407.py
+++ b/saleor/checkout/migrations/0002_auto_20170206_0407.py
@@ -11,7 +11,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0001_auto_20170113_0435'),
+        ('checkout', '0001_auto_20170113_0435'),
+    ]
+
+    replaces = [
+        ('cart', '0002_auto_20170206_0407'),
     ]
 
     operations = [
@@ -51,7 +55,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='cartline',
             name='cart',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='cart.Cart', verbose_name='cart'),
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='checkout.Cart', verbose_name='cart'),
         ),
         migrations.AlterField(
             model_name='cartline',
diff --git a/saleor/cart/migrations/0003_auto_20170906_0556.py b/saleor/checkout/migrations/0003_auto_20170906_0556.py
similarity index 86%
rename from saleor/cart/migrations/0003_auto_20170906_0556.py
rename to saleor/checkout/migrations/0003_auto_20170906_0556.py
index eafccaae730..ce16d6fd784 100644
--- a/saleor/cart/migrations/0003_auto_20170906_0556.py
+++ b/saleor/checkout/migrations/0003_auto_20170906_0556.py
@@ -8,7 +8,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0002_auto_20170206_0407'),
+        ('checkout', '0002_auto_20170206_0407'),
+    ]
+
+    replaces = [
+        ('cart', '0003_auto_20170906_0556'),
     ]
 
     operations = [
diff --git a/saleor/cart/migrations/0004_auto_20171129_1004.py b/saleor/checkout/migrations/0004_auto_20171129_1004.py
similarity index 80%
rename from saleor/cart/migrations/0004_auto_20171129_1004.py
rename to saleor/checkout/migrations/0004_auto_20171129_1004.py
index 8a710801487..919d5392a88 100644
--- a/saleor/cart/migrations/0004_auto_20171129_1004.py
+++ b/saleor/checkout/migrations/0004_auto_20171129_1004.py
@@ -8,7 +8,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0003_auto_20170906_0556'),
+        ('checkout', '0003_auto_20170906_0556'),
+    ]
+
+    replaces = [
+        ('cart', '0004_auto_20171129_1004'),
     ]
 
     operations = [
diff --git a/saleor/cart/migrations/0005_auto_20180108_0814.py b/saleor/checkout/migrations/0005_auto_20180108_0814.py
similarity index 95%
rename from saleor/cart/migrations/0005_auto_20180108_0814.py
rename to saleor/checkout/migrations/0005_auto_20180108_0814.py
index 548d4afd319..b4c581bcb3c 100644
--- a/saleor/cart/migrations/0005_auto_20180108_0814.py
+++ b/saleor/checkout/migrations/0005_auto_20180108_0814.py
@@ -15,7 +15,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0004_auto_20171129_1004'),
+        ('checkout', '0004_auto_20171129_1004'),
+    ]
+
+    replaces = [
+        ('cart', '0005_auto_20180108_0814'),
     ]
 
     operations = [
@@ -72,7 +76,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='cartline',
             name='cart',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='cart.Cart'),
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='checkout.Cart'),
         ),
         migrations.AlterField(
             model_name='cartline',
diff --git a/saleor/cart/migrations/0006_auto_20180221_0825.py b/saleor/checkout/migrations/0006_auto_20180221_0825.py
similarity index 79%
rename from saleor/cart/migrations/0006_auto_20180221_0825.py
rename to saleor/checkout/migrations/0006_auto_20180221_0825.py
index ac4b2bca3fb..5121c97ea16 100644
--- a/saleor/cart/migrations/0006_auto_20180221_0825.py
+++ b/saleor/checkout/migrations/0006_auto_20180221_0825.py
@@ -6,7 +6,11 @@
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0005_auto_20180108_0814'),
+        ('checkout', '0005_auto_20180108_0814'),
+    ]
+
+    replaces = [
+        ('cart', '0006_auto_20180221_0825'),
     ]
 
     operations = [
diff --git a/saleor/checkout/migrations/0007_merge_cart_with_checkout.py b/saleor/checkout/migrations/0007_merge_cart_with_checkout.py
new file mode 100644
index 00000000000..9702e3a865a
--- /dev/null
+++ b/saleor/checkout/migrations/0007_merge_cart_with_checkout.py
@@ -0,0 +1,81 @@
+# Generated by Django 2.0.3 on 2018-05-29 08:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django_prices.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('account', '0019_auto_20180528_1205'),
+        ('shipping', '0008_auto_20180108_0814'),
+        ('checkout', '0006_auto_20180221_0825'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='cart',
+            options={},
+        ),
+        migrations.RemoveField(
+            model_name='cart',
+            name='checkout_data',
+        ),
+        migrations.RemoveField(
+            model_name='cart',
+            name='last_status_change',
+        ),
+        migrations.RemoveField(
+            model_name='cart',
+            name='status',
+        ),
+        migrations.RemoveField(
+            model_name='cart',
+            name='total',
+        ),
+        migrations.RemoveField(
+            model_name='cart',
+            name='voucher',
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='billing_address',
+            field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='account.Address'),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='discount_amount',
+            field=django_prices.models.MoneyField(currency='USD', decimal_places=2, default=0, max_digits=12),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='discount_name',
+            field=models.CharField(blank=True, max_length=255, null=True),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='note',
+            field=models.TextField(blank=True, default=''),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='shipping_address',
+            field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='account.Address'),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='shipping_method',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='carts', to='shipping.ShippingMethodCountry'),
+        ),
+        migrations.AddField(
+            model_name='cart',
+            name='voucher_code',
+            field=models.CharField(blank=True, max_length=12, null=True),
+        ),
+        migrations.AlterField(
+            model_name='cart',
+            name='email',
+            field=models.EmailField(blank=True, default='', max_length=254),
+        ),
+    ]
diff --git a/saleor/checkout/migrations/0008_rename_tables.py b/saleor/checkout/migrations/0008_rename_tables.py
new file mode 100644
index 00000000000..70927ce7594
--- /dev/null
+++ b/saleor/checkout/migrations/0008_rename_tables.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.0.3 on 2018-05-29 08:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('checkout', '0007_merge_cart_with_checkout'),
+    ]
+
+    operations = [
+        migrations.AlterModelTable(
+            name='cart',
+            table=None,
+        ),
+        migrations.AlterModelTable(
+            name='cartline',
+            table=None,
+        ),
+    ]
diff --git a/saleor/cart/migrations/__init__.py b/saleor/checkout/migrations/__init__.py
similarity index 100%
rename from saleor/cart/migrations/__init__.py
rename to saleor/checkout/migrations/__init__.py
diff --git a/saleor/cart/migrations/fix_empty_data_in_lines.py b/saleor/checkout/migrations/fix_empty_data_in_lines.py
similarity index 67%
rename from saleor/cart/migrations/fix_empty_data_in_lines.py
rename to saleor/checkout/migrations/fix_empty_data_in_lines.py
index 7b0c33e2e53..eed49f4956f 100644
--- a/saleor/cart/migrations/fix_empty_data_in_lines.py
+++ b/saleor/checkout/migrations/fix_empty_data_in_lines.py
@@ -6,7 +6,7 @@
 
 
 def convert_lines_data(apps, schema_editor):
-    CartLine = apps.get_model('cart', 'CartLine')
+    CartLine = apps.get_model('checkout', 'CartLine')
     # Iterate over all cart lines, due to wrong JSONField None handling
     for line in CartLine.objects.all():
         if line.data is None:
@@ -17,9 +17,13 @@ def convert_lines_data(apps, schema_editor):
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('cart', '0002_auto_20161014_1221'),
+        ('checkout', '0002_auto_20161014_1221'),
+    ]
+
+    replaces = [
+        ('cart', 'fix_empty_data_in_lines'),
     ]
 
     operations = [
-        migrations.RunPython(convert_lines_data)
+        migrations.RunPython(convert_lines_data, migrations.RunPython.noop)
     ]
diff --git a/saleor/checkout/models.py b/saleor/checkout/models.py
new file mode 100644
index 00000000000..0629eda0c51
--- /dev/null
+++ b/saleor/checkout/models.py
@@ -0,0 +1,148 @@
+"""Cart-related ORM models."""
+from decimal import Decimal
+from uuid import uuid4
+
+from django.conf import settings
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.utils.encoding import smart_str
+from django_prices.models import MoneyField
+from jsonfield import JSONField
+
+from ..account.models import Address
+from ..core.utils.taxes import ZERO_TAXED_MONEY
+from ..shipping.models import ShippingMethodCountry
+
+CENTS = Decimal('0.01')
+
+
+class CartQueryset(models.QuerySet):
+    """A specialized queryset for dealing with carts."""
+
+    def for_display(self):
+        """Annotate the queryset for display purposes.
+
+        Prefetches additional data from the database to avoid the n+1 queries
+        problem.
+        """
+        return self.prefetch_related(
+            'lines__variant__product__category',
+            'lines__variant__product__images',
+            'lines__variant__product__product_type__product_attributes__values')  # noqa
+
+
+class Cart(models.Model):
+    """A shopping cart."""
+
+    created = models.DateTimeField(auto_now_add=True)
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL, blank=True, null=True, related_name='carts',
+        on_delete=models.CASCADE)
+    email = models.EmailField(blank=True, default='')
+    token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
+    quantity = models.PositiveIntegerField(default=0)
+    billing_address = models.ForeignKey(
+        Address, related_name='+', editable=False, null=True,
+        on_delete=models.SET_NULL)
+    shipping_address = models.ForeignKey(
+        Address, related_name='+', editable=False, null=True,
+        on_delete=models.SET_NULL)
+    shipping_method = models.ForeignKey(
+        ShippingMethodCountry, blank=True, null=True, related_name='carts',
+        on_delete=models.SET_NULL)
+    note = models.TextField(blank=True, default='')
+    discount_amount = MoneyField(
+        currency=settings.DEFAULT_CURRENCY, max_digits=12,
+        decimal_places=settings.DEFAULT_DECIMAL_PLACES, default=0)
+    discount_name = models.CharField(max_length=255, blank=True, null=True)
+    voucher_code = models.CharField(max_length=12, blank=True, null=True)
+
+    objects = CartQueryset.as_manager()
+
+    def __repr__(self):
+        return 'Cart(quantity=%s)' % (self.quantity,)
+
+    def __iter__(self):
+        return iter(self.lines.all())
+
+    def __len__(self):
+        return self.lines.count()
+
+    def is_shipping_required(self):
+        """Return `True` if any of the lines requires shipping."""
+        return any(line.is_shipping_required() for line in self)
+
+    def get_shipping_price(self, taxes):
+        return (
+            self.shipping_method.get_total_price(taxes)
+            if self.shipping_method and self.is_shipping_required()
+            else ZERO_TAXED_MONEY)
+
+    def get_subtotal(self, discounts=None, taxes=None):
+        """Return the total cost of the cart prior to shipping."""
+        subtotals = (line.get_total(discounts, taxes) for line in self)
+        return sum(subtotals, ZERO_TAXED_MONEY)
+
+    def get_total(self, discounts=None, taxes=None):
+        """Return the total cost of the cart."""
+        return (
+            self.get_subtotal(discounts, taxes)
+            + self.get_shipping_price(taxes)
+            - self.discount_amount)
+
+    def get_line(self, variant):
+        """Return a line matching the given variant and data if any."""
+        matching_lines = (line for line in self if line.variant == variant)
+        return next(matching_lines, None)
+
+
+class CartLine(models.Model):
+    """A single cart line.
+
+    Multiple lines in the same cart can refer to the same product variant if
+    their `data` field is different.
+    """
+
+    cart = models.ForeignKey(
+        Cart, related_name='lines', on_delete=models.CASCADE)
+    variant = models.ForeignKey(
+        'product.ProductVariant', related_name='+', on_delete=models.CASCADE)
+    quantity = models.PositiveIntegerField(
+        validators=[MinValueValidator(0), MaxValueValidator(999)])
+    data = JSONField(blank=True, default={})
+
+    class Meta:
+        unique_together = ('cart', 'variant', 'data')
+
+    def __str__(self):
+        return smart_str(self.variant)
+
+    def __eq__(self, other):
+        if not isinstance(other, CartLine):
+            return NotImplemented
+
+        return (
+            self.variant == other.variant and
+            self.quantity == other.quantity)
+
+    def __ne__(self, other):
+        return not self == other  # pragma: no cover
+
+    def __repr__(self):
+        return 'CartLine(variant=%r, quantity=%r)' % (
+            self.variant, self.quantity)
+
+    def __getstate__(self):
+        return self.variant, self.quantity
+
+    def __setstate__(self, data):
+        self.variant, self.quantity = data
+
+    def get_total(self, discounts=None, taxes=None):
+        """Return the total price of this line."""
+        amount = self.quantity * self.variant.get_price(discounts, taxes)
+        return amount.quantize(CENTS)
+
+    def is_shipping_required(self):
+        """Return `True` if the related product variant requires shipping."""
+        return self.variant.is_shipping_required()
diff --git a/saleor/checkout/urls.py b/saleor/checkout/urls.py
index 849ef982a59..3b1e7339f13 100644
--- a/saleor/checkout/urls.py
+++ b/saleor/checkout/urls.py
@@ -1,14 +1,24 @@
 from django.conf.urls import url
 
 from . import views
+from .views.discount import remove_voucher_view
 
-urlpatterns = [
-    url(r'^$', views.index_view, name='index'),
-    url(r'^shipping-address/', views.shipping_address_view,
+checkout_urlpatterns = [
+    url(r'^$', views.checkout_index, name='index'),
+    url(r'^shipping-address/', views.checkout_shipping_address,
         name='shipping-address'),
-    url(r'^shipping-method/', views.shipping_method_view,
+    url(r'^shipping-method/', views.checkout_shipping_method,
         name='shipping-method'),
-    url(r'^summary/', views.summary_view, name='summary'),
-    url(r'^remove_voucher/', views.discount.remove_voucher_view,
+    url(r'^summary/', views.checkout_summary, name='summary'),
+    url(r'^remove_voucher/', remove_voucher_view,
         name='remove-voucher'),
-    url(r'^login/', views.login, name='login')]
+    url(r'^login/', views.checkout_login, name='login')]
+
+
+cart_urlpatterns = [
+    url(r'^$', views.cart_index, name='index'),
+    url(r'^update/(?P<variant_id>\d+)/$',
+        views.update_cart_line, name='update-line'),
+    url(r'^summary/$', views.cart_summary, name='summary'),
+    url(r'^shipping-options/$', views.cart_shipping_options,
+        name='shipping-options')]
diff --git a/saleor/checkout/utils.py b/saleor/checkout/utils.py
index 54d2d1a7b56..7ba9a629862 100644
--- a/saleor/checkout/utils.py
+++ b/saleor/checkout/utils.py
@@ -1,27 +1,619 @@
-from django.utils.translation import pgettext
+"""Cart-related utility functions."""
+from datetime import date, timedelta
+from functools import wraps
+from uuid import UUID
 
-from ..cart.utils import (
-    get_category_variants_and_prices, get_product_variants_and_prices)
+from django.conf import settings
+from django.contrib import messages
+from django.db import transaction
+from django.db.models import Sum
+from django.utils.encoding import smart_text
+from django.utils.translation import get_language, pgettext, pgettext_lazy
+from prices import TaxedMoneyRange
+
+from . import AddressType, logger
+from ..account.forms import get_address_form
+from ..account.models import Address
+from ..account.utils import store_user_address
+from ..core.exceptions import InsufficientStock
+from ..core.utils import to_local_currency
+from ..core.utils.taxes import ZERO_MONEY, get_taxes_for_country
 from ..discount import VoucherType
-from ..discount.models import NotApplicable
+from ..discount.models import NotApplicable, Voucher
 from ..discount.utils import (
     get_product_or_category_voucher_discount, get_shipping_voucher_discount,
-    get_value_voucher_discount)
+    get_value_voucher_discount, increase_voucher_usage)
+from ..order.models import Order
+from ..shipping.models import ShippingMethodCountry
+from .forms import (
+    AddressChoiceForm, AnonymousUserBillingForm, AnonymousUserShippingForm,
+    BillingAddressChoiceForm)
+from .models import Cart
+
+COOKIE_NAME = 'cart'
+
+
+def set_cart_cookie(simple_cart, response):
+    """Update response with a cart token cookie."""
+    # FIXME: document why session is not used
+    max_age = int(timedelta(days=30).total_seconds())
+    response.set_signed_cookie(COOKIE_NAME, simple_cart.token, max_age=max_age)
+
+
+def contains_unavailable_variants(cart):
+    """Return `True` if cart contains any unfulfillable lines."""
+    try:
+        for line in cart:
+            line.variant.check_quantity(line.quantity)
+    except InsufficientStock:
+        return True
+    return False
+
+
+def token_is_valid(token):
+    """Validate a cart token."""
+    if token is None:
+        return False
+    if isinstance(token, UUID):
+        return True
+    try:
+        UUID(token)
+    except ValueError:
+        return False
+    return True
+
+
+def remove_unavailable_variants(cart):
+    """Remove any unavailable items from cart."""
+    for line in cart:
+        try:
+            add_variant_to_cart(
+                cart, line.variant, line.quantity, replace=True)
+        except InsufficientStock as e:
+            quantity = e.item.quantity_available
+            add_variant_to_cart(cart, line.variant, quantity, replace=True)
+
+
+def get_product_variants_and_prices(cart, product):
+    """Get variants and unit prices from cart lines matching the product."""
+    lines = (line for line in cart if line.variant.product_id == product.id)
+    for line in lines:
+        for dummy_i in range(line.quantity):
+            yield line.variant, line.variant.get_price()
+
+
+def get_category_variants_and_prices(cart, root_category):
+    """Get variants and unit prices from cart lines matching the category.
+
+    Product is assumed to be in the category if it belongs to any of its
+    descendant subcategories.
+    """
+    matching_products = {
+        line.variant.product for line in cart
+        if line.variant.product.category.is_descendant_of(
+            root_category, include_self=True)}
+    for product in matching_products:
+        for line in get_product_variants_and_prices(cart, product):
+            yield line
+
+
+def check_product_availability_and_warn(request, cart):
+    """Warn if cart contains any lines that cannot be fulfilled."""
+    if contains_unavailable_variants(cart):
+        msg = pgettext_lazy(
+            'Cart warning message',
+            'Sorry. We don\'t have that many items in stock. '
+            'Quantity was set to maximum available for now.')
+        messages.warning(request, msg)
+        remove_unavailable_variants(cart)
+
+
+def find_and_assign_anonymous_cart(queryset=Cart.objects.all()):
+    """Assign cart from cookie to request user."""
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            response = view(request, *args, **kwargs)
+            token = request.get_signed_cookie(COOKIE_NAME, default=None)
+            if not token_is_valid(token):
+                return response
+            cart = get_anonymous_cart_from_token(
+                token=token, cart_queryset=queryset)
+            if cart is None:
+                return response
+            if request.user.is_authenticated:
+                with transaction.atomic():
+                    change_cart_user(cart, request.user)
+                    carts_to_close = Cart.objects.filter(user=request.user)
+                    carts_to_close = carts_to_close.exclude(token=token)
+                    carts_to_close.delete()
+                response.delete_cookie(COOKIE_NAME)
+            return response
+
+        return func
+    return get_cart
+
+
+def get_or_create_anonymous_cart_from_token(
+        token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token or create a new one."""
+    return cart_queryset.filter(token=token, user=None).get_or_create(
+        defaults={'user': None})[0]
+
+
+def get_or_create_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user or create a new one."""
+    defaults = {
+        'shipping_address': user.default_shipping_address,
+        'billing_address': user.default_billing_address}
+    return cart_queryset.get_or_create(user=user, defaults=defaults)[0]
+
+
+def get_anonymous_cart_from_token(token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token if any."""
+    return cart_queryset.filter(token=token, user=None).first()
+
+
+def get_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user if any."""
+    return cart_queryset.filter(user=user).first()
+
+
+def get_or_create_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or create a new one based on cookie."""
+    if request.user.is_authenticated:
+        return get_or_create_user_cart(request.user, cart_queryset)
+    token = request.get_signed_cookie(COOKIE_NAME, default=None)
+    return get_or_create_anonymous_cart_from_token(token, cart_queryset)
+
+
+def get_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or return a new instance based on cookie."""
+    if request.user.is_authenticated:
+        cart = get_user_cart(request.user, cart_queryset)
+        user = request.user
+    else:
+        token = request.get_signed_cookie(COOKIE_NAME, default=None)
+        cart = get_anonymous_cart_from_token(token, cart_queryset)
+        user = None
+    if cart is not None:
+        return cart
+    if user:
+        return Cart(user=user)
+    return Cart()
+
+
+def get_or_create_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to always receive a saved cart instance.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, one will be created and a cookie will be set
+    for users who are not logged in.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_or_create_cart_from_request(request, cart_queryset)
+            response = view(request, cart, *args, **kwargs)
+            if not request.user.is_authenticated:
+                set_cart_cookie(cart, response)
+            return response
+        return func
+    return get_cart
+
+
+def get_or_empty_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to receive a cart if one exists.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, an unsaved `Cart` instance will be used.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_cart_from_request(request, cart_queryset)
+            return view(request, cart, *args, **kwargs)
+        return func
+    return get_cart
+
+
+def get_cart_data(cart, shipping_range, currency, discounts, taxes):
+    """Return a JSON-serializable representation of the cart."""
+    cart_total = None
+    local_cart_total = None
+    shipping_required = False
+    total_with_shipping = None
+    local_total_with_shipping = None
+    if cart:
+        cart_total = cart.get_subtotal(discounts, taxes)
+        local_cart_total = to_local_currency(cart_total, currency)
+        shipping_required = cart.is_shipping_required()
+        total_with_shipping = TaxedMoneyRange(
+            start=cart_total, stop=cart_total)
+        if shipping_required and shipping_range:
+            total_with_shipping = shipping_range + cart_total
+        local_total_with_shipping = to_local_currency(
+            total_with_shipping, currency)
+
+    return {
+        'cart_total': cart_total,
+        'local_cart_total': local_cart_total,
+        'shipping_required': shipping_required,
+        'total_with_shipping': total_with_shipping,
+        'local_total_with_shipping': local_total_with_shipping}
+
+
+def find_open_cart_for_user(user):
+    """Find an open cart for the given user."""
+    carts = user.carts.all()
+    if len(carts) > 1:
+        logger.warning('%s has more than one open basket', user)
+        for cart in carts[1:]:
+            cart.delete()
+    return carts.first()
+
+
+def change_cart_user(cart, user):
+    """Assign cart to a user.
+
+    If the user already has an open cart assigned, cancel it.
+    """
+    open_cart = find_open_cart_for_user(user)
+    if open_cart is not None:
+        open_cart.delete()
+    cart.user = user
+    cart.shipping_address = user.default_shipping_address
+    cart.billing_address = user.default_billing_address
+    cart.save(update_fields=['user', 'shipping_address', 'billing_address'])
+
+
+def update_cart_quantity(cart):
+    """Update the total quantity in cart."""
+    total_lines = cart.lines.aggregate(
+        total_quantity=Sum('quantity'))['total_quantity']
+    if not total_lines:
+        total_lines = 0
+    cart.quantity = total_lines
+    cart.save(update_fields=['quantity'])
+
+
+def add_variant_to_cart(
+        cart, variant, quantity=1, replace=False, check_quantity=True):
+    """Add a product variant to cart.
+
+    The `data` parameter may be used to differentiate between items with
+    different customization options.
+
+    If `replace` is truthy then any previous quantity is discarded instead
+    of added to.
+    """
+    line, _ = cart.lines.get_or_create(
+        variant=variant, defaults={'quantity': 0, 'data': {}})
+    new_quantity = quantity if replace else (quantity + line.quantity)
+
+    if new_quantity < 0:
+        raise ValueError('%r is not a valid quantity (results in %r)' % (
+            quantity, new_quantity))
+
+    if new_quantity == 0:
+        line.delete()
+    else:
+        if check_quantity:
+            variant.check_quantity(new_quantity)
+
+        line.quantity = new_quantity
+        line.save(update_fields=['quantity'])
+
+    update_cart_quantity(cart)
+
+
+def get_shipping_address_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on shipping address in cart."""
+    shipping_address = (
+        cart.shipping_address or cart.user.default_shipping_address)
+
+    if shipping_address and shipping_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses,
+            initial={'address': shipping_address.id})
+    elif shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            instance=shipping_address)
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+
+    return address_form, addresses_form, preview
+
+
+def update_shipping_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_shipping_address_forms(cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def update_shipping_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_address_form(
+        data, country_code=country.code,
+        autocomplete_type='shipping',
+        instance=cart.shipping_address,
+        initial={'country': country})
+    user_form = AnonymousUserShippingForm(
+        data if not preview else None, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid():
+        user_form.save()
+        address = address_form.save()
+        change_shipping_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
 
+def get_billing_forms_with_shipping(cart, data, user_addresses, country):
+    """Get billing form based on a the current billing and shipping data."""
+    shipping_address = cart.shipping_address
+    billing_address = cart.billing_address or Address(country=country)
 
-def _get_value_voucher_discount_for_checkout(voucher, checkout):
+    if not billing_address.id or billing_address == shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': shipping_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.SHIPPING_ADDRESS})
+    elif billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': billing_address.id})
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.NEW_ADDRESS})
+
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart_with_shipping(
+        cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = get_billing_forms_with_shipping(
+        cart, data, user_addresses, country)
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        address = None
+        address_id = addresses_form.cleaned_data['address']
+
+        if address_id == BillingAddressChoiceForm.SHIPPING_ADDRESS:
+            if cart.user and cart.shipping_address in user_addresses:
+                address = cart.shipping_address
+            else:
+                address = cart.shipping_address.get_copy()
+        elif address_id != BillingAddressChoiceForm.NEW_ADDRESS:
+            address = user_addresses.get(id=address_id)
+        elif address_form.is_valid():
+            address = address_form.save()
+
+        if address:
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def get_anonymous_summary_without_shipping_forms(cart, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing', instance=billing_address)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            autocomplete_type='billing', initial={'country': country})
+
+    return address_form, preview
+
+
+def update_billing_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_anonymous_summary_without_shipping_forms(
+        cart, data, country)
+    user_form = AnonymousUserBillingForm(data, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid() and not preview:
+        user_form.save()
+        address = address_form.save()
+        change_billing_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
+
+def get_summary_without_shipping_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address and billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country})
+        initial_address = billing_address.id
+    elif billing_address:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        initial_address = AddressChoiceForm.NEW_ADDRESS
+    else:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=country.code,
+            initial={'country': country})
+        if cart.user and cart.user.default_billing_address:
+            initial_address = cart.user.default_billing_address.id
+        else:
+            initial_address = AddressChoiceForm.NEW_ADDRESS
+
+    addresses_form = AddressChoiceForm(
+        data, addresses=user_addresses, initial={'address': initial_address})
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_summary_without_shipping_forms(
+            cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid():
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def change_billing_address_in_cart(cart, address):
+    """Save billing address in cart if changed.
+
+    Remove previously saved address if not connected to any user.
+    """
+    has_address_changed = (
+        not address and cart.billing_address or
+        address and not cart.billing_address or
+        address and cart.billing_address and address != cart.billing_address)
+    if has_address_changed:
+        remove_old_address = (
+            cart.billing_address and (not cart.user or (
+                cart.user and
+                cart.billing_address not in cart.user.addresses.all())))
+        if remove_old_address:
+            cart.billing_address.delete()
+        cart.billing_address = address
+        cart.save(update_fields=['billing_address'])
+
+
+def change_shipping_address_in_cart(cart, address):
+    """Save shipping address in cart if changed.
+
+    Remove previously saved address if not connected to any user.
+    """
+    has_address_changed = (
+        not address and cart.shipping_address or
+        address and not cart.shipping_address or
+        address and cart.shipping_address and address != cart.shipping_address)
+    if has_address_changed:
+        remove_old_address = (
+            cart.shipping_address and (not cart.user or (
+                cart.user and
+                cart.shipping_address not in cart.user.addresses.all())))
+        if remove_old_address:
+            cart.shipping_address.delete()
+        cart.shipping_address = address
+        cart.save(update_fields=['shipping_address'])
+
+
+def get_cart_data_for_checkout(cart, discounts, taxes):
+    """Data shared between views in checkout process."""
+    lines = [(line, line.get_total(discounts, taxes)) for line in cart]
+    subtotal = cart.get_subtotal(discounts, taxes)
+    total = cart.get_total(discounts, taxes)
+    shipping_price = cart.get_shipping_price(taxes)
+    return {
+        'cart': cart,
+        'cart_are_taxes_handled': bool(taxes),
+        'cart_lines': lines,
+        'cart_shipping_price': shipping_price,
+        'cart_subtotal': subtotal,
+        'cart_total': total}
+
+
+def _get_value_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value for a voucher of value type."""
-    return get_value_voucher_discount(voucher, checkout.get_subtotal())
+    return get_value_voucher_discount(voucher, cart.get_subtotal())
 
 
-def _get_shipping_voucher_discount_for_checkout(voucher, checkout):
+def _get_shipping_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value for a voucher of shipping type."""
-    if not checkout.is_shipping_required:
+    if not cart.is_shipping_required():
         msg = pgettext(
             'Voucher not applicable',
             'Your order does not require shipping.')
         raise NotApplicable(msg)
-    shipping_method = checkout.shipping_method
+    shipping_method = cart.shipping_method
     if not shipping_method:
         msg = pgettext(
             'Voucher not applicable',
@@ -36,19 +628,19 @@ def _get_shipping_voucher_discount_for_checkout(voucher, checkout):
         raise NotApplicable(
             msg % {'country': voucher.get_apply_to_display()})
     return get_shipping_voucher_discount(
-        voucher, checkout.get_subtotal(), shipping_method.get_total_price())
+        voucher, cart.get_subtotal(), shipping_method.get_total_price())
 
 
-def _get_product_or_category_voucher_discount_for_checkout(voucher, checkout):
+def _get_product_or_category_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value for a voucher of product or category type."""
     if voucher.type == VoucherType.PRODUCT:
         prices = [
             variant_price for _, variant_price in
-            get_product_variants_and_prices(checkout.cart, voucher.product)]
+            get_product_variants_and_prices(cart, voucher.product)]
     else:
         prices = [
             variant_price for _, variant_price in
-            get_category_variants_and_prices(checkout.cart, voucher.category)]
+            get_category_variants_and_prices(cart, voucher.category)]
     if not prices:
         msg = pgettext(
             'Voucher not applicable',
@@ -57,16 +649,162 @@ def _get_product_or_category_voucher_discount_for_checkout(voucher, checkout):
     return get_product_or_category_voucher_discount(voucher, prices)
 
 
-def get_voucher_discount_for_checkout(voucher, checkout):
+def get_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value depending on voucher and discount types.
 
     Raise NotApplicable if voucher of given type cannot be applied.
     """
     if voucher.type == VoucherType.VALUE:
-        return _get_value_voucher_discount_for_checkout(voucher, checkout)
+        return _get_value_voucher_discount_for_cart(voucher, cart)
     if voucher.type == VoucherType.SHIPPING:
-        return _get_shipping_voucher_discount_for_checkout(voucher, checkout)
+        return _get_shipping_voucher_discount_for_cart(voucher, cart)
     if voucher.type in (VoucherType.PRODUCT, VoucherType.CATEGORY):
-        return _get_product_or_category_voucher_discount_for_checkout(
-            voucher, checkout)
+        return _get_product_or_category_voucher_discount_for_cart(
+            voucher, cart)
     raise NotImplementedError('Unknown discount type')
+
+
+def get_voucher_for_cart(cart, vouchers=None):
+    """Return voucher with voucher code saved in cart if active or None."""
+    if cart.voucher_code is not None:
+        if vouchers is None:
+            vouchers = Voucher.objects.active(date=date.today())
+        try:
+            return vouchers.get(code=cart.voucher_code)
+        except Voucher.DoesNotExist:
+            return None
+    return None
+
+
+def recalculate_cart_discount(cart):
+    """Recalculate `cart.discount` based on the voucher.
+
+    Will clear both voucher and discount if the discount is no longer
+    applicable.
+    """
+    voucher = get_voucher_for_cart(cart)
+    if voucher is not None:
+        try:
+            cart.discount_amount = get_voucher_discount_for_cart(voucher, cart)
+            cart.discount_name = voucher.name
+            cart.save(update_fields=['discount_amount', 'discount_name'])
+        except NotApplicable:
+            remove_voucher_from_cart(cart)
+    else:
+        remove_voucher_from_cart(cart)
+
+
+def remove_voucher_from_cart(cart):
+    """Remove voucher data from cart."""
+    cart.voucher_code = None
+    cart.discount_name = None
+    cart.discount_amount = ZERO_MONEY
+    cart.save(update_fields=[
+        'voucher_code', 'discount_name', 'discount_amount'])
+
+
+def get_taxes_for_cart(cart, default_taxes):
+    """Return taxes (if handled) due to shipping address or default one."""
+    if not settings.VATLAYER_ACCESS_KEY:
+        return None
+
+    if cart.shipping_address:
+        return get_taxes_for_country(cart.shipping_address.country)
+
+    return default_taxes
+
+
+def check_shipping_method(cart):
+    """Check if shipping method is valid for address and remove (if not)."""
+    country_code = cart.shipping_address.country.code
+    valid_methods = ShippingMethodCountry.objects.select_related(
+        'shipping_method').unique_for_country_code(country_code)
+    if cart.shipping_method not in valid_methods:
+        cart.shipping_method = None
+        cart.save()
+        return False
+    return True
+
+
+@transaction.atomic
+def create_order(cart, tracking_code, discounts, taxes):
+    """Create an order from the cart.
+
+    Each order will get a private copy of both the billing and the shipping
+    address (if shipping).
+
+    If any of the addresses is new and the user is logged in the address
+    will also get saved to that user's address book.
+
+    Current user's language is saved in the order so we can later determine
+    which language to use when sending email.
+    """
+    # FIXME: save locale along with the language
+    from ..order.utils import add_variant_to_order
+
+    voucher = get_voucher_for_cart(
+        cart, vouchers=Voucher.objects.active(
+            date=date.today()).select_for_update())
+
+    if cart.voucher_code and not voucher:
+        # Voucher expired in meantime, abort order placement
+        return None
+
+    billing_address = cart.billing_address
+
+    if cart.is_shipping_required():
+        shipping_address = cart.shipping_address
+        shipping_method = cart.shipping_method
+        shipping_method_name = smart_text(shipping_method)
+    else:
+        shipping_address = None
+        shipping_method = None
+        shipping_method_name = None
+
+    if cart.user:
+        store_user_address(cart.user, billing_address, AddressType.BILLING)
+
+        if cart.user.addresses.filter(pk=billing_address.pk).exists():
+            billing_address = billing_address.get_copy()
+
+        if cart.is_shipping_required():
+            store_user_address(
+                cart.user, shipping_address, AddressType.SHIPPING)
+
+            if cart.user.addresses.filter(pk=shipping_address.pk).exists():
+                shipping_address = shipping_address.get_copy()
+
+    order_data = {
+        'language_code': get_language(),
+        'billing_address': billing_address,
+        'shipping_address': shipping_address,
+        'tracking_client_id': tracking_code,
+        'shipping_method': shipping_method,
+        'shipping_method_name': shipping_method_name,
+        'shipping_price': cart.get_shipping_price(taxes),
+        'total': cart.get_total(taxes=taxes)}
+
+    if cart.user:
+        order_data['user'] = cart.user
+        order_data['user_email'] = cart.user.email
+    else:
+        order_data['user_email'] = cart.email
+
+    if voucher:
+        order_data['voucher'] = voucher
+        order_data['discount_amount'] = cart.discount_amount
+        order_data['discount_name'] = cart.discount_name
+
+    order = Order.objects.create(**order_data)
+
+    for line in cart:
+        add_variant_to_order(
+            order, line.variant, line.quantity, discounts, taxes)
+
+    if voucher:
+        increase_voucher_usage(voucher)
+
+    if cart.note:
+        order.notes.create(user=order.user, content=cart.note)
+
+    return order
diff --git a/saleor/checkout/views/__init__.py b/saleor/checkout/views/__init__.py
index 469550c3158..b8054ce6d07 100644
--- a/saleor/checkout/views/__init__.py
+++ b/saleor/checkout/views/__init__.py
@@ -1,82 +1,221 @@
-from django.shortcuts import redirect
+"""Cart and checkout related views."""
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404, redirect, render
 from django.template.response import TemplateResponse
 
+from ...account.forms import LoginForm
+from ...core.utils import (
+    format_money, get_user_shipping_country, to_local_currency)
+from ...product.models import ProductVariant
+from ...shipping.utils import get_shipment_options
+from ..forms import CartShippingMethodForm, CountryForm, ReplaceCartLineForm
+from ..models import Cart
+from ..utils import (
+    check_product_availability_and_warn, check_shipping_method, get_cart_data,
+    get_cart_data_for_checkout, get_or_empty_db_cart, get_taxes_for_cart)
 from .discount import add_voucher_form, validate_voucher
-from .shipping import (anonymous_user_shipping_address_view,
-                       user_shipping_address_view)
+from .shipping import (
+    anonymous_user_shipping_address_view, user_shipping_address_view)
 from .summary import (
-    summary_with_shipping_view, anonymous_summary_without_shipping,
+    anonymous_summary_without_shipping, summary_with_shipping_view,
     summary_without_shipping)
 from .validators import (
-    validate_cart, validate_shipping_address,
-    validate_shipping_method, validate_is_shipping_required)
-from ..core import load_checkout
-from ..forms import ShippingMethodForm
-from ...account.forms import LoginForm
+    validate_cart, validate_is_shipping_required, validate_shipping_address,
+    validate_shipping_method)
+
+
+@get_or_empty_db_cart(Cart.objects.for_display())
+@validate_cart
+def checkout_login(request, cart):
+    """Allow the user to log in prior to checkout."""
+    if request.user.is_authenticated:
+        return redirect('checkout:index')
+    ctx = {'form': LoginForm()}
+    return TemplateResponse(request, 'checkout/login.html', ctx)
 
 
-@load_checkout
+@get_or_empty_db_cart(Cart.objects.for_display())
 @validate_cart
 @validate_is_shipping_required
-def index_view(request, checkout):
+def checkout_index(request, cart):
     """Redirect to the initial step of checkout."""
     return redirect('checkout:shipping-address')
 
 
-@load_checkout
+@get_or_empty_db_cart(Cart.objects.for_display())
 @validate_voucher
 @validate_cart
 @validate_is_shipping_required
 @add_voucher_form
-def shipping_address_view(request, checkout):
+def checkout_shipping_address(request, cart):
     """Display the correct shipping address step."""
     if request.user.is_authenticated:
-        return user_shipping_address_view(request, checkout)
-    return anonymous_user_shipping_address_view(request, checkout)
+        return user_shipping_address_view(request, cart)
+    return anonymous_user_shipping_address_view(request, cart)
 
 
-@load_checkout
+@get_or_empty_db_cart(Cart.objects.for_display())
 @validate_voucher
 @validate_cart
 @validate_is_shipping_required
 @validate_shipping_address
 @add_voucher_form
-def shipping_method_view(request, checkout):
+def checkout_shipping_method(request, cart):
     """Display the shipping method selection step."""
-    country_code = checkout.shipping_address.country.code
-    shipping_method_form = ShippingMethodForm(
-        country_code, request.taxes, request.POST or None,
-        initial={'method': checkout.shipping_method})
-    if shipping_method_form.is_valid():
-        checkout.shipping_method = shipping_method_form.cleaned_data['method']
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    check_shipping_method(cart)
+    form = CartShippingMethodForm(
+        request.POST or None, taxes=taxes, instance=cart,
+        initial={'shipping_method': cart.shipping_method})
+
+    if form.is_valid():
+        form.save()
         return redirect('checkout:summary')
-    return TemplateResponse(
-        request, 'checkout/shipping_method.html',
-        context={
-            'shipping_method_form': shipping_method_form,
-            'checkout': checkout})
 
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({'shipping_method_form': form})
+    return TemplateResponse(request, 'checkout/shipping_method.html', ctx)
 
-@load_checkout
+
+@get_or_empty_db_cart(Cart.objects.for_display())
 @validate_voucher
 @validate_cart
 @add_voucher_form
-def summary_view(request, checkout):
+def checkout_summary(request, cart):
     """Display the correct order summary."""
-    if checkout.is_shipping_required:
+    if cart.is_shipping_required():
         view = validate_shipping_address(summary_with_shipping_view)
         view = validate_shipping_method(view)
-        return view(request, checkout)
+        return view(request, cart)
     if request.user.is_authenticated:
-        return summary_without_shipping(request, checkout)
-    return anonymous_summary_without_shipping(request, checkout)
+        return summary_without_shipping(request, cart)
+    return anonymous_summary_without_shipping(request, cart)
 
 
-@load_checkout
-@validate_cart
-def login(request, checkout):
-    """Allow the user to log in prior to checkout."""
-    if request.user.is_authenticated:
+@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
+def cart_index(request, cart):
+    """Display cart details."""
+    discounts = request.discounts
+    taxes = request.taxes
+    cart_lines = []
+    check_product_availability_and_warn(request, cart)
+
+    # refresh required to get updated cart lines and it's quantity
+    try:
+        cart = Cart.objects.prefetch_related(
+            'lines__variant__product__category').get(pk=cart.pk)
+    except Cart.DoesNotExist:
+        pass
+
+    lines = cart.lines.select_related(
+        'variant__product__product_type',
+        'variant__product__category')
+    lines = lines.prefetch_related(
+        'variant__product__images',
+        'variant__product__product_type__variant_attributes')
+    for line in lines:
+        initial = {'quantity': line.quantity}
+        form = ReplaceCartLineForm(
+            None, cart=cart, variant=line.variant, initial=initial,
+            discounts=discounts, taxes=taxes)
+        cart_lines.append({
+            'variant': line.variant,
+            'get_price': line.variant.get_price(discounts, taxes),
+            'get_total': line.get_total(discounts, taxes),
+            'form': form})
+
+    default_country = get_user_shipping_country(request)
+    country_form = CountryForm(initial={'country': default_country})
+    default_country_options = get_shipment_options(default_country, taxes)
+
+    cart_data = get_cart_data(
+        cart, default_country_options, request.currency, discounts, taxes)
+    ctx = {
+        'cart_lines': cart_lines,
+        'country_form': country_form,
+        'default_country_options': default_country_options}
+    ctx.update(cart_data)
+
+    return TemplateResponse(request, 'checkout/index.html', ctx)
+
+
+@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
+def cart_shipping_options(request, cart):
+    """Display shipping options to get a price estimate."""
+    country_form = CountryForm(request.POST or None, taxes=request.taxes)
+    if country_form.is_valid():
+        shipments = country_form.get_shipment_options()
+    else:
+        shipments = None
+    ctx = {
+        'default_country_options': shipments,
+        'country_form': country_form}
+    cart_data = get_cart_data(
+        cart, shipments, request.currency, request.discounts, request.taxes)
+    ctx.update(cart_data)
+    return TemplateResponse(request, 'checkout/_subtotal_table.html', ctx)
+
+
+@get_or_empty_db_cart()
+def update_cart_line(request, cart, variant_id):
+    """Update the line quantities."""
+    if not request.is_ajax():
         return redirect('checkout:index')
-    form = LoginForm()
-    return TemplateResponse(request, 'checkout/login.html', {'form': form})
+    variant = get_object_or_404(ProductVariant, pk=variant_id)
+    discounts = request.discounts
+    taxes = request.taxes
+    status = None
+    form = ReplaceCartLineForm(
+        request.POST, cart=cart, variant=variant, discounts=discounts,
+        taxes=taxes)
+    if form.is_valid():
+        form.save()
+        response = {
+            'variantId': variant_id,
+            'subtotal': 0,
+            'total': 0,
+            'cart': {
+                'numItems': cart.quantity,
+                'numLines': len(cart)}}
+        updated_line = cart.get_line(form.cart_line.variant)
+        if updated_line:
+            response['subtotal'] = format_money(
+                updated_line.get_total(discounts, taxes).gross)
+        if cart:
+            cart_total = cart.get_subtotal(discounts, taxes)
+            response['total'] = format_money(cart_total.gross)
+            local_cart_total = to_local_currency(cart_total, request.currency)
+            if local_cart_total is not None:
+                response['localTotal'] = format_money(local_cart_total.gross)
+        status = 200
+    elif request.POST is not None:
+        response = {'error': form.errors}
+        status = 400
+    return JsonResponse(response, status=status)
+
+
+@get_or_empty_db_cart(cart_queryset=Cart.objects.for_display())
+def cart_summary(request, cart):
+    """Display a cart summary suitable for displaying on all pages."""
+    discounts = request.discounts
+    taxes = request.taxes
+
+    def prepare_line_data(line):
+        first_image = line.variant.get_first_image()
+        return {
+            'product': line.variant.product,
+            'variant': line.variant.name,
+            'quantity': line.quantity,
+            'image': first_image,
+            'line_total': line.get_total(discounts, taxes),
+            'variant_url': line.variant.get_absolute_url()}
+
+    if cart.quantity == 0:
+        data = {'quantity': 0}
+    else:
+        data = {
+            'quantity': cart.quantity,
+            'total': cart.get_subtotal(discounts, taxes),
+            'lines': [prepare_line_data(line) for line in cart]}
+
+    return render(request, 'cart_dropdown.html', data)
diff --git a/saleor/checkout/views/discount.py b/saleor/checkout/views/discount.py
index 1f06b0dcc18..831f03d4081 100644
--- a/saleor/checkout/views/discount.py
+++ b/saleor/checkout/views/discount.py
@@ -7,38 +7,36 @@
 from django.utils.translation import pgettext
 from django.views.decorators.http import require_POST
 
-from ...discount.forms import CheckoutDiscountForm
 from ...discount.models import Voucher
-from ..core import load_checkout
+from ..forms import CartVoucherForm
+from ..models import Cart
+from ..utils import (
+    get_or_empty_db_cart, recalculate_cart_discount, remove_voucher_from_cart)
 
 
 def add_voucher_form(view):
     """Decorate a view injecting a voucher form and handling its submission."""
     @wraps(view)
-    def func(request, checkout):
+    def func(request, cart):
         prefix = 'discount'
         data = {k: v for k, v in request.POST.items() if k.startswith(prefix)}
-        voucher_form = CheckoutDiscountForm(
-            data or None, checkout=checkout, prefix=prefix)
+        voucher_form = CartVoucherForm(
+            data or None, prefix=prefix, instance=cart)
         if voucher_form.is_bound:
             if voucher_form.is_valid():
-                voucher_form.apply_discount()
+                voucher_form.save()
                 next_url = request.GET.get(
                     'next', request.META['HTTP_REFERER'])
                 return redirect(next_url)
             else:
-                del checkout.discount
-                del checkout.discount_name
-                del checkout.voucher_code
+                remove_voucher_from_cart(cart)
                 # if only discount form was used we clear post for other forms
                 request.POST = {}
         else:
-            checkout.recalculate_discount()
-        response = view(request, checkout)
+            recalculate_cart_discount(cart)
+        response = view(request, cart)
         if isinstance(response, TemplateResponse):
-            voucher = voucher_form.initial.get('voucher')
             response.context_data['voucher_form'] = voucher_form
-            response.context_data['voucher'] = voucher
         return response
     return func
 
@@ -50,29 +48,26 @@ def validate_voucher(view):
     redirected to the checkout summary view.
     """
     @wraps(view)
-    def func(request, checkout, cart):
-        if checkout.voucher_code:
+    def func(request, cart):
+        if cart.voucher_code:
             try:
                 Voucher.objects.active(date=date.today()).get(
-                    code=checkout.voucher_code)
+                    code=cart.voucher_code)
             except Voucher.DoesNotExist:
-                del checkout.voucher_code
-                checkout.recalculate_discount()
+                remove_voucher_from_cart(cart)
                 msg = pgettext(
                     'Checkout warning',
                     'This voucher has expired. Please review your checkout.')
                 messages.warning(request, msg)
                 return redirect('checkout:summary')
-        return view(request, checkout, cart)
+        return view(request, cart)
     return func
 
 
 @require_POST
-@load_checkout
-def remove_voucher_view(request, checkout, cart):
+@get_or_empty_db_cart(Cart.objects.for_display())
+def remove_voucher_view(request, cart):
     """Clear the discount and remove the voucher."""
     next_url = request.GET.get('next', request.META['HTTP_REFERER'])
-    del checkout.discount
-    del checkout.discount_name
-    del checkout.voucher_code
+    remove_voucher_from_cart(cart)
     return redirect(next_url)
diff --git a/saleor/checkout/views/shipping.py b/saleor/checkout/views/shipping.py
index 3c3da5ad14c..989404cb310 100644
--- a/saleor/checkout/views/shipping.py
+++ b/saleor/checkout/views/shipping.py
@@ -1,74 +1,48 @@
 from django.shortcuts import redirect
 from django.template.response import TemplateResponse
 
-from ...account.forms import get_address_form
-from ...account.models import Address
-from ..forms import AnonymousUserShippingForm, ShippingAddressesForm
+from ..utils import (
+    get_cart_data_for_checkout, get_taxes_for_cart,
+    update_shipping_address_in_anonymous_cart, update_shipping_address_in_cart)
 
 
-def anonymous_user_shipping_address_view(request, checkout):
+def anonymous_user_shipping_address_view(request, cart):
     """Display the shipping step for a user who is not logged in."""
-    address_form, preview = get_address_form(
-        request.POST or None, country_code=request.country.code,
-        autocomplete_type='shipping',
-        initial={'country': request.country.code},
-        instance=checkout.shipping_address)
+    user_form, address_form, updated = (
+        update_shipping_address_in_anonymous_cart(
+            cart, request.POST or None, request.country))
 
-    user_form = AnonymousUserShippingForm(
-        not preview and request.POST or None, initial={'email': checkout.email}
-        if not preview else request.POST.dict())
-    if all([user_form.is_valid(), address_form.is_valid()]):
-        checkout.shipping_address = address_form.instance
-        checkout.email = user_form.cleaned_data['email']
+    if updated:
         return redirect('checkout:shipping-method')
-    return TemplateResponse(
-        request, 'checkout/shipping_address.html', context={
-            'address_form': address_form, 'user_form': user_form,
-            'checkout': checkout})
 
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({
+        'address_form': address_form,
+        'user_form': user_form})
+    return TemplateResponse(request, 'checkout/shipping_address.html', ctx)
 
-def user_shipping_address_view(request, checkout):
+
+def user_shipping_address_view(request, cart):
     """Display the shipping step for a logged in user.
 
     In addition to entering a new address the user has an option of selecting
     one of the existing entries from their address book.
     """
-    data = request.POST or None
-    additional_addresses = request.user.addresses.all()
-    checkout.email = request.user.email
-    shipping_address = checkout.shipping_address
+    cart.email = request.user.email
+    cart.save(update_fields=['email'])
+    user_addresses = cart.user.addresses.all()
+
+    addresses_form, address_form, updated = update_shipping_address_in_cart(
+        cart, user_addresses, request.POST or None, request.country)
 
-    if shipping_address is not None and shipping_address.id:
-        address_form, preview = get_address_form(
-            data, country_code=request.country.code,
-            initial={'country': request.country})
-        addresses_form = ShippingAddressesForm(
-            data, additional_addresses=additional_addresses,
-            initial={'address': shipping_address.id})
-    elif shipping_address:
-        address_form, preview = get_address_form(
-            data, country_code=shipping_address.country.code,
-            instance=shipping_address)
-        addresses_form = ShippingAddressesForm(
-            data, additional_addresses=additional_addresses)
-    else:
-        address_form, preview = get_address_form(
-            data, initial={'country': request.country},
-            country_code=request.country.code)
-        addresses_form = ShippingAddressesForm(
-            data, additional_addresses=additional_addresses)
+    if updated:
+        return redirect('checkout:shipping-method')
 
-    if addresses_form.is_valid() and not preview:
-        if (addresses_form.cleaned_data['address'] !=
-                ShippingAddressesForm.NEW_ADDRESS):
-            address_id = addresses_form.cleaned_data['address']
-            checkout.shipping_address = Address.objects.get(id=address_id)
-            return redirect('checkout:shipping-method')
-        elif address_form.is_valid():
-            checkout.shipping_address = address_form.instance
-            return redirect('checkout:shipping-method')
-    return TemplateResponse(
-        request, 'checkout/shipping_address.html', context={
-            'address_form': address_form, 'user_form': addresses_form,
-            'checkout': checkout,
-            'additional_addresses': additional_addresses})
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({
+        'additional_addresses': user_addresses,
+        'address_form': address_form,
+        'user_form': addresses_form})
+    return TemplateResponse(request, 'checkout/shipping_address.html', ctx)
diff --git a/saleor/checkout/views/summary.py b/saleor/checkout/views/summary.py
index 222a49c5aeb..0e5385b912d 100644
--- a/saleor/checkout/views/summary.py
+++ b/saleor/checkout/views/summary.py
@@ -3,192 +3,122 @@
 from django.template.response import TemplateResponse
 from django.utils.translation import pgettext, pgettext_lazy
 
-from ...account.forms import get_address_form
 from ...account.models import Address
+from ...core import analytics
 from ...core.exceptions import InsufficientStock
 from ...order.emails import send_order_confirmation
-from ..forms import (
-    AnonymousUserBillingForm, BillingAddressesForm,
-    BillingWithoutShippingAddressForm, NoteForm)
+from ..forms import CartNoteForm
+from ..utils import (
+    create_order, get_cart_data_for_checkout, get_taxes_for_cart,
+    update_billing_address_in_anonymous_cart, update_billing_address_in_cart,
+    update_billing_address_in_cart_with_shipping)
 
 
-def create_order(checkout):
-    """Finalize a checkout session and create an order.
-
-    This is a helper function.
-
-    `checkout` is a `saleor.checkout.core.Checkout` instance.
-    """
-    order = checkout.create_order()
-    if not order:
-        return None, redirect('checkout:summary')
-    checkout.clear_storage()
-    checkout.cart.clear()
-    user = None if checkout.user.is_anonymous else checkout.user
-    msg = pgettext_lazy('Order status history entry', 'Order was placed')
-    order.history.create(user=user, content=msg)
-    send_order_confirmation.delay(order.pk)
-    return order, redirect('order:payment', token=order.token)
-
-
-def handle_order_placement(request, checkout):
+def handle_order_placement(request, cart):
     """Try to create an order and redirect the user as necessary.
 
     This is a helper function.
     """
     try:
-        order, redirect_url = create_order(checkout)
+        order = create_order(
+            cart=cart,
+            tracking_code=analytics.get_client_id(request),
+            discounts=request.discounts,
+            taxes=get_taxes_for_cart(cart, request.taxes))
     except InsufficientStock:
-        return redirect('cart:index')
+        return redirect('checkout:index')
+
     if not order:
         msg = pgettext('Checkout warning', 'Please review your checkout.')
         messages.warning(request, msg)
-    return redirect_url
-
-
-def get_billing_forms_with_shipping(
-        data, addresses, billing_address, shipping_address):
-    """Get billing form based on a the current billing and shipping data."""
-    if billing_address == shipping_address:
-        address_form, preview = get_address_form(
-            data, country_code=shipping_address.country.code,
-            autocomplete_type='billing',
-            initial={'country': shipping_address.country.code},
-            instance=None)
-        addresses_form = BillingAddressesForm(
-            data, additional_addresses=addresses, initial={
-                'address': BillingAddressesForm.SHIPPING_ADDRESS})
-    elif billing_address.id is None:
-        address_form, preview = get_address_form(
-            data, country_code=billing_address.country.code,
-            autocomplete_type='billing',
-            initial={'country': billing_address.country.code},
-            instance=billing_address)
-        addresses_form = BillingAddressesForm(
-            data, additional_addresses=addresses, initial={
-                'address': BillingAddressesForm.NEW_ADDRESS})
-    else:
-        address_form, preview = get_address_form(
-            data, country_code=billing_address.country.code,
-            autocomplete_type='billing',
-            initial={'country': billing_address.country})
-        addresses_form = BillingAddressesForm(
-            data, additional_addresses=addresses, initial={
-                'address': billing_address.id})
-    if addresses_form.is_valid() and not preview:
-        address_id = addresses_form.cleaned_data['address']
-        if address_id == BillingAddressesForm.SHIPPING_ADDRESS:
-            return address_form, addresses_form, shipping_address
-        elif address_id != BillingAddressesForm.NEW_ADDRESS:
-            address = addresses.get(id=address_id)
-            return address_form, addresses_form, address
-        elif address_form.is_valid():
-            return address_form, addresses_form, address_form.instance
-    return address_form, addresses_form, None
-
-
-def summary_with_shipping_view(request, checkout):
+        return redirect('checkout:summary')
+
+    user = cart.user
+    cart.delete()
+    msg = pgettext_lazy('Order status history entry', 'Order was placed')
+    order.history.create(user=user, content=msg)
+    send_order_confirmation.delay(order.pk)
+    return redirect('order:payment', token=order.token)
+
+
+def summary_with_shipping_view(request, cart):
     """Display order summary with billing forms for a logged in user.
 
     Will create an order if all data is valid.
     """
-    note_form = NoteForm(request.POST or None, checkout=checkout)
+    note_form = CartNoteForm(request.POST or None, instance=cart)
     if note_form.is_valid():
-        note_form.set_checkout_note()
-
-    if request.user.is_authenticated:
-        additional_addresses = request.user.addresses.all()
-    else:
-        additional_addresses = Address.objects.none()
-    address_form, addresses_form, address = get_billing_forms_with_shipping(
-        request.POST or None, additional_addresses,
-        checkout.billing_address or Address(country=request.country),
-        checkout.shipping_address)
-    if address is not None:
-        checkout.billing_address = address
-        return handle_order_placement(request, checkout)
-    return TemplateResponse(
-        request, 'checkout/summary.html', context={
-            'addresses_form': addresses_form, 'address_form': address_form,
-            'checkout': checkout,
-            'additional_addresses': additional_addresses,
-            'note_form': note_form})
+        note_form.save()
+
+    user_addresses = (
+        cart.user.addresses.all() if cart.user else Address.objects.none())
+
+    addresses_form, address_form, updated = (
+        update_billing_address_in_cart_with_shipping(
+            cart, user_addresses, request.POST or None, request.country))
+
+    if updated:
+        return handle_order_placement(request, cart)
+
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({
+        'additional_addresses': user_addresses,
+        'address_form': address_form,
+        'addresses_form': addresses_form,
+        'note_form': note_form})
+    return TemplateResponse(request, 'checkout/summary.html', ctx)
 
 
-def anonymous_summary_without_shipping(request, checkout):
+def anonymous_summary_without_shipping(request, cart):
     """Display order summary with billing forms for an unauthorized user.
 
     Will create an order if all data is valid.
     """
-    note_form = NoteForm(request.POST or None, checkout=checkout)
+    note_form = CartNoteForm(request.POST or None, instance=cart)
     if note_form.is_valid():
-        note_form.set_checkout_note()
-    user_form = AnonymousUserBillingForm(
-        request.POST or None, initial={'email': checkout.email})
-    billing_address = checkout.billing_address
-    if billing_address:
-        address_form, preview = get_address_form(
-            request.POST or None, country_code=billing_address.country.code,
-            autocomplete_type='billing', instance=billing_address)
-    else:
-        address_form, preview = get_address_form(
-            request.POST or None, country_code=request.country.code,
-            autocomplete_type='billing', initial={'country': request.country})
-    if all([user_form.is_valid(), address_form.is_valid()]) and not preview:
-        checkout.email = user_form.cleaned_data['email']
-        checkout.billing_address = address_form.instance
-        return handle_order_placement(request, checkout)
+        note_form.save()
+
+    user_form, address_form, updated = (
+        update_billing_address_in_anonymous_cart(
+            cart, request.POST or None, request.country))
+
+    if updated:
+        return handle_order_placement(request, cart)
+
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({
+        'address_form': address_form,
+        'note_form': note_form,
+        'user_form': user_form})
     return TemplateResponse(
-        request, 'checkout/summary_without_shipping.html', context={
-            'user_form': user_form, 'address_form': address_form,
-            'checkout': checkout,
-            'note_form': note_form})
+        request, 'checkout/summary_without_shipping.html', ctx)
 
 
-def summary_without_shipping(request, checkout):
+def summary_without_shipping(request, cart):
     """Display order summary for cases where shipping is not required.
 
     Will create an order if all data is valid.
     """
-    note_form = NoteForm(request.POST or None, checkout=checkout)
+    note_form = CartNoteForm(request.POST or None, instance=cart)
     if note_form.is_valid():
-        note_form.set_checkout_note()
-
-    billing_address = checkout.billing_address
-    user_addresses = request.user.addresses.all()
-    if billing_address and billing_address.id:
-        address_form, preview = get_address_form(
-            request.POST or None, autocomplete_type='billing',
-            initial={'country': request.country},
-            country_code=billing_address.country.code)
-        addresses_form = BillingWithoutShippingAddressForm(
-            request.POST or None, additional_addresses=user_addresses,
-            initial={'address': billing_address.id})
-    elif billing_address:
-        address_form, preview = get_address_form(
-            request.POST or None, autocomplete_type='billing',
-            instance=billing_address,
-            country_code=billing_address.country.code)
-        addresses_form = BillingWithoutShippingAddressForm(
-            request.POST or None, additional_addresses=user_addresses)
-    else:
-        address_form, preview = get_address_form(
-            request.POST or None, autocomplete_type='billing',
-            initial={'country': request.country},
-            country_code=request.country.code)
-        addresses_form = BillingWithoutShippingAddressForm(
-            request.POST or None, additional_addresses=user_addresses)
-
-    if addresses_form.is_valid():
-        address_id = addresses_form.cleaned_data['address']
-        if address_id != BillingWithoutShippingAddressForm.NEW_ADDRESS:
-            checkout.billing_address = user_addresses.get(id=address_id)
-            return handle_order_placement(request, checkout)
-        elif address_form.is_valid() and not preview:
-            checkout.billing_address = address_form.instance
-            return handle_order_placement(request, checkout)
+        note_form.save()
+
+    user_addresses = cart.user.addresses.all()
+
+    addresses_form, address_form, updated = update_billing_address_in_cart(
+        cart, user_addresses, request.POST or None, request.country)
+
+    if updated:
+        return handle_order_placement(request, cart)
+
+    taxes = get_taxes_for_cart(cart, request.taxes)
+    ctx = get_cart_data_for_checkout(cart, request.discounts, taxes)
+    ctx.update({
+        'additional_addresses': user_addresses,
+        'address_form': address_form,
+        'addresses_form': addresses_form,
+        'note_form': note_form})
     return TemplateResponse(
-        request, 'checkout/summary_without_shipping.html', context={
-            'addresses_form': addresses_form, 'address_form': address_form,
-            'checkout': checkout, 'additional_addresses': user_addresses,
-            'note_form': note_form})
+        request, 'checkout/summary_without_shipping.html', ctx)
diff --git a/saleor/checkout/views/validators.py b/saleor/checkout/views/validators.py
index 6d590227da7..5b134225bce 100644
--- a/saleor/checkout/views/validators.py
+++ b/saleor/checkout/views/validators.py
@@ -3,70 +3,68 @@
 from django.core.exceptions import ValidationError
 from django.shortcuts import redirect
 
+from ..utils import check_shipping_method
+
 
 def validate_cart(view):
     """Decorate a view making it require a non-empty cart.
 
-    Expects to be decorated with `@load_checkout`.
-
-    Changes view signature from `func(request, checkout, cart)` to
-    `func(request, checkout)`.
-
-    If the cart is empty redirects to the cart details.
+    If the cart is empty, redirect to the cart details.
     """
     @wraps(view)
-    def func(request, checkout, cart):
+    def func(request, cart):
         if cart:
-            return view(request, checkout)
-        return redirect('cart:index')
+            return view(request, cart)
+        return redirect('checkout:index')
     return func
 
 
 def validate_shipping_address(view):
     """Decorate a view making it require a valid shipping address.
 
-    Expects to be decorated with `@validate_cart`.
-
-    If either the shipping address or customer email is empty redirects to the
+    If either the shipping address or customer email is empty, redirect to the
     shipping address step.
+
+    Expects to be decorated with `@validate_cart`.
     """
     @wraps(view)
-    def func(request, checkout):
-        if checkout.email is None or checkout.shipping_address is None:
+    def func(request, cart):
+        if not cart.email or not cart.shipping_address:
             return redirect('checkout:shipping-address')
         try:
-            checkout.shipping_address.full_clean()
+            cart.shipping_address.full_clean()
         except ValidationError:
             return redirect('checkout:shipping-address')
-        return view(request, checkout)
+        return view(request, cart)
     return func
 
 
 def validate_shipping_method(view):
     """Decorate a view making it require a shipping method.
 
-    Expects to be decorated with `@validate_cart`.
+    If the method is missing or incorrect, redirect to the shipping method
+    step.
 
-    If the method is missing redirects to the shipping method step.
+    Expects to be decorated with `@validate_cart`.
     """
     @wraps(view)
-    def func(request, checkout):
-        if checkout.shipping_method is None:
+    def func(request, cart):
+        if not cart.shipping_method or not check_shipping_method(cart):
             return redirect('checkout:shipping-method')
-        return view(request, checkout)
+        return view(request, cart)
     return func
 
 
 def validate_is_shipping_required(view):
-    """Decorate a view making it check if checkout needs shipping.
+    """Decorate a view making it check if cart in checkout needs shipping.
 
-    Expects to be decorated with `@validate_cart`.
+    If shipping is not needed, redirect to the checkout summary.
 
-    If shipping is not needed redirects to the checkout summary.
+    Expects to be decorated with `@validate_cart`.
     """
     @wraps(view)
-    def func(request, checkout):
-        if not checkout.is_shipping_required:
+    def func(request, cart):
+        if not cart.is_shipping_required():
             return redirect('checkout:summary')
-        return view(request, checkout)
+        return view(request, cart)
     return func
diff --git a/saleor/core/utils/random_data.py b/saleor/core/utils/random_data.py
index f32a4703232..da4d53a1733 100644
--- a/saleor/core/utils/random_data.py
+++ b/saleor/core/utils/random_data.py
@@ -16,6 +16,7 @@
 
 from ...account.models import Address, User
 from ...account.utils import store_user_address
+from ...checkout import AddressType
 from ...core.utils.taxes import get_tax_rate_by_name, get_taxes_for_country
 from ...core.utils.text import strip_html_and_truncate
 from ...discount import DiscountValueType, VoucherType
@@ -556,7 +557,8 @@ def set_featured_products(how_many=8):
 def add_address_to_admin(email):
     address = create_address()
     user = User.objects.get(email=email)
-    store_user_address(user, address, True, True)
+    store_user_address(user, address, AddressType.BILLING)
+    store_user_address(user, address, AddressType.SHIPPING)
 
 
 def create_fake_collection(placeholder_dir, collection_data):
diff --git a/saleor/dashboard/order/forms.py b/saleor/dashboard/order/forms.py
index 43b0c7c9a93..e1f820b36e4 100644
--- a/saleor/dashboard/order/forms.py
+++ b/saleor/dashboard/order/forms.py
@@ -9,7 +9,7 @@
 from ...account.i18n import (
     AddressForm as StorefrontAddressForm, PossiblePhoneNumberFormField)
 from ...account.models import User
-from ...cart.forms import QuantityField
+from ...checkout.forms import QuantityField
 from ...core.exceptions import InsufficientStock
 from ...core.utils.taxes import ZERO_TAXED_MONEY
 from ...discount.models import Voucher
@@ -459,7 +459,7 @@ def clean(self):
             raise forms.ValidationError(
                 pgettext_lazy(
                     'Cancel order form error',
-                    "This order can't be cancelled"))
+                    "This order can't be canceled"))
         return data
 
     def cancel_order(self):
diff --git a/saleor/dashboard/order/urls.py b/saleor/dashboard/order/urls.py
index d1bd755560b..039d191c1de 100644
--- a/saleor/dashboard/order/urls.py
+++ b/saleor/dashboard/order/urls.py
@@ -14,7 +14,7 @@
     url(r'^(?P<order_pk>\d+)/cancel/$',
         views.cancel_order, name='order-cancel'),
     url(r'^(?P<order_pk>\d+)/address/(?P<address_type>billing|shipping)/$',
-        views.address_view, name='address-edit'),
+        views.order_address, name='address-edit'),
     url(r'^(?P<order_pk>\d+)/edit-customer/$',
         views.order_customer_edit, name='order-customer-edit'),
     url(r'^(?P<order_pk>\d+)/remove-customer/$',
diff --git a/saleor/dashboard/order/utils.py b/saleor/dashboard/order/utils.py
index faf910c032c..44ebb215b2c 100644
--- a/saleor/dashboard/order/utils.py
+++ b/saleor/dashboard/order/utils.py
@@ -2,6 +2,7 @@
 from django.contrib.sites.shortcuts import get_current_site
 from django.template.loader import get_template
 
+from ...checkout import AddressType
 from ...core.utils.taxes import ZERO_MONEY
 from ...discount import VoucherType
 from ...discount.models import NotApplicable
@@ -156,7 +157,7 @@ def save_address_in_order(order, address, address_type):
 
     If the other type of address is empty, copy it.
     """
-    if address_type == 'shipping':
+    if address_type == AddressType.SHIPPING:
         order.shipping_address = address
         if not order.billing_address:
             order.billing_address = address.get_copy()
diff --git a/saleor/dashboard/order/views.py b/saleor/dashboard/order/views.py
index 25e8618160e..f7a93df3293 100644
--- a/saleor/dashboard/order/views.py
+++ b/saleor/dashboard/order/views.py
@@ -280,7 +280,7 @@ def orderline_cancel(request, order_pk, line_pk):
     if form.is_valid():
         msg = pgettext_lazy(
             'Dashboard message related to an order line',
-            'Cancelled item %s') % line
+            'Canceled item %s') % line
         with transaction.atomic():
             form.cancel_line()
             messages.success(request, msg)
@@ -330,7 +330,7 @@ def add_variant_to_order(request, order_pk):
 
 @staff_member_required
 @permission_required('order.edit_order')
-def address_view(request, order_pk, address_type):
+def order_address(request, order_pk, address_type):
     order = get_object_or_404(Order, pk=order_pk)
     update_prices = False
     if address_type == 'shipping':
diff --git a/saleor/discount/forms.py b/saleor/discount/forms.py
deleted file mode 100644
index 4db214b3f6a..00000000000
--- a/saleor/discount/forms.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from datetime import date
-
-from django import forms
-from django.utils.encoding import smart_text
-from django.utils.translation import pgettext_lazy
-
-from ..checkout.utils import get_voucher_discount_for_checkout
-from .models import NotApplicable, Voucher
-
-
-class VoucherField(forms.ModelChoiceField):
-
-    default_error_messages = {
-        'invalid_choice': pgettext_lazy(
-            'Voucher form error', 'Discount code incorrect or expired'),
-    }
-
-
-class CheckoutDiscountForm(forms.Form):
-
-    voucher = VoucherField(
-        queryset=Voucher.objects.none(),
-        to_field_name='code',
-        help_text=pgettext_lazy(
-            'Checkout discount form label for voucher field',
-            'Gift card or discount code'),
-        widget=forms.TextInput)
-
-    def __init__(self, *args, **kwargs):
-        self.checkout = kwargs.pop('checkout')
-        initial = kwargs.get('initial', {})
-        if 'voucher' not in initial:
-            initial['voucher'] = self.checkout.voucher_code
-        kwargs['initial'] = initial
-        super().__init__(*args, **kwargs)
-        self.fields['voucher'].queryset = Voucher.objects.active(
-            date=date.today())
-
-    def clean(self):
-        cleaned_data = super().clean()
-        if 'voucher' in cleaned_data:
-            voucher = cleaned_data['voucher']
-            try:
-                discount = get_voucher_discount_for_checkout(
-                    voucher, self.checkout)
-                cleaned_data['discount'] = discount
-                cleaned_data['discount_name'] = voucher.name
-            except NotApplicable as e:
-                self.add_error('voucher', smart_text(e))
-        return cleaned_data
-
-    def apply_discount(self):
-        discount = self.cleaned_data['discount']
-        discount_name = self.cleaned_data['discount_name']
-        voucher = self.cleaned_data['voucher']
-        self.checkout.discount = discount
-        self.checkout.discount_name = discount_name
-        self.checkout.voucher_code = voucher.code
diff --git a/saleor/order/utils.py b/saleor/order/utils.py
index e48971b9751..cb6e96b86b7 100644
--- a/saleor/order/utils.py
+++ b/saleor/order/utils.py
@@ -1,9 +1,9 @@
 from functools import wraps
 
-from django.db.models import F
 from django.shortcuts import get_object_or_404, redirect
 
 from ..account.utils import store_user_address
+from ..checkout import AddressType
 from ..core.exceptions import InsufficientStock
 from ..core.utils.taxes import (
     ZERO_MONEY, get_tax_rate_by_name, get_taxes_for_address)
@@ -137,86 +137,37 @@ def cancel_fulfillment(fulfillment, restock):
 
 
 def attach_order_to_user(order, user):
-    """Associates existing order with user account."""
+    """Associate existing order with user account."""
     order.user = user
-    store_user_address(user, order.billing_address, billing=True)
+    store_user_address(user, order.billing_address, AddressType.BILLING)
     if order.shipping_address:
-        store_user_address(user, order.shipping_address, shipping=True)
+        store_user_address(user, order.shipping_address, AddressType.SHIPPING)
     order.save(update_fields=['user'])
 
 
-def add_variant_to_order(
-        order, variant, total_quantity, discounts=None, taxes=None,
-        add_to_existing=True):
+def add_variant_to_order(order, variant, quantity, discounts=None, taxes=None):
     """Add total_quantity of variant to order.
 
     Raises InsufficientStock exception if quantity could not be fulfilled.
-
-    By default, first adds variant to existing lines with same variant.
-    It can be disabled with setting add_to_existing to False.
-
-    Order lines are created by increasing quantity of lines,
-    as long as total_quantity of variant will be added.
     """
-    quantity_not_fulfilled = add_variant_to_existing_lines(
-        order, variant, total_quantity) if add_to_existing else total_quantity
-
-    if not quantity_not_fulfilled:
-        return
-    if quantity_not_fulfilled > variant.quantity_available:
+    if quantity > variant.quantity_available:
         raise InsufficientStock(variant)
 
-    order.lines.create(
-        product_name=variant.display_product(),
-        product_sku=variant.sku,
-        is_shipping_required=variant.is_shipping_required(),
-        quantity=quantity_not_fulfilled,
-        variant=variant,
-        unit_price=variant.get_price(discounts, taxes),
-        tax_rate=get_tax_rate_by_name(variant.product.tax_rate, taxes))
-    allocate_stock(variant, quantity_not_fulfilled)
-
-
-def add_variant_to_existing_lines(order, variant, total_quantity):
-    """Add variant to existing lines with same variant.
-
-    Variant is added by increasing quantity of lines with same variant,
-    as long as total_quantity of variant will be added
-    or there is no more lines with same variant.
-
-    Returns quantity that could not be fulfilled with existing lines.
-    """
-    # order descending by lines' stock available quantity
-    lines = order.lines.filter(variant=variant).order_by(
-        F('variant__quantity_allocated') - F('variant__quantity'))
-
-    quantity_left = total_quantity
-    for line in lines:
-        quantity = (
-            line.variant.quantity_available
-            if quantity_left > line.variant.quantity_available
-            else quantity_left)
+    try:
+        line = order.lines.get(variant=variant)
         line.quantity += quantity
-        line.save()
-        allocate_stock(line.variant, quantity)
-        quantity_left -= quantity
-        if quantity_left == 0:
-            break
-    return quantity_left
-
-
-def merge_duplicates_into_order_line(line):
-    """Merge duplicated lines in order into one (given) line.
-
-    If there are no duplicates, nothing will happen.
-    """
-    lines = line.order.lines.filter(
-        product_name=line.product_name, product_sku=line.product_sku,
-        variant=line.variant, is_shipping_required=line.is_shipping_required)
-    if lines.count() > 1:
-        line.quantity = sum([line.quantity for line in lines])
         line.save(update_fields=['quantity'])
-        lines.exclude(pk=line.pk).delete()
+    except OrderLine.DoesNotExist:
+        order.lines.create(
+            product_name=variant.display_product(),
+            product_sku=variant.sku,
+            is_shipping_required=variant.is_shipping_required(),
+            quantity=quantity,
+            variant=variant,
+            unit_price=variant.get_price(discounts, taxes),
+            tax_rate=get_tax_rate_by_name(variant.product.tax_rate, taxes))
+
+    allocate_stock(variant, quantity)
 
 
 def change_order_line_quantity(line, new_quantity):
@@ -240,10 +191,9 @@ def restock_order_lines(order):
                 line.save(update_fields=['quantity_fulfilled'])
 
 
-def restock_fulfillment_lines(fulfillment, allocate=True):
+def restock_fulfillment_lines(fulfillment):
     """Return fulfilled products to corresponding stocks."""
     for line in fulfillment:
-        order_line = line.order_line
-        if order_line.variant:
+        if line.order_line.variant:
             increase_stock(
-                line.order_line.variant, line.quantity, allocate=allocate)
+                line.order_line.variant, line.quantity, allocate=True)
diff --git a/saleor/product/forms.py b/saleor/product/forms.py
index 79fb0443e92..1574a3bad0c 100644
--- a/saleor/product/forms.py
+++ b/saleor/product/forms.py
@@ -5,7 +5,7 @@
 from django.utils.translation import pgettext_lazy
 from django_prices.templatetags.prices_i18n import amount
 
-from ..cart.forms import AddToCartForm
+from ..checkout.forms import AddToCartForm
 from ..core.utils.taxes import display_gross_prices
 
 
diff --git a/saleor/product/urls.py b/saleor/product/urls.py
index 25af552dcbf..8f1f39accb0 100644
--- a/saleor/product/urls.py
+++ b/saleor/product/urls.py
@@ -8,6 +8,6 @@
     url(r'^category/(?P<path>[a-z0-9-_/]+?)-(?P<category_id>[0-9]+)/$',
         views.category_index, name='category'),
     url(r'(?P<slug>[a-z0-9-_]+?)-(?P<product_id>[0-9]+)/add/$',
-        views.product_add_to_cart, name="add-to-cart"),
+        views.product_add_to_cart, name='add-to-cart'),
     url(r'^collection/(?P<slug>[a-z0-9-_/]+?)-(?P<pk>[0-9]+)/$',
         views.collection_index, name='collection')]
diff --git a/saleor/product/utils/__init__.py b/saleor/product/utils/__init__.py
index 96dbfd57fdc..91c52992fcc 100644
--- a/saleor/product/utils/__init__.py
+++ b/saleor/product/utils/__init__.py
@@ -4,7 +4,7 @@
 from django.contrib.auth.models import AnonymousUser
 from django.db.models import F
 
-from ...cart.utils import (
+from ...checkout.utils import (
     get_cart_from_request, get_or_create_cart_from_request)
 from ...core.utils import get_paginator_items
 from ...core.utils.filters import get_now_sorted_by
diff --git a/saleor/product/views.py b/saleor/product/views.py
index 8912a0c4ee9..38061bca9e7 100644
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -6,11 +6,11 @@
 from django.template.response import TemplateResponse
 from django.urls import reverse
 
-from ..cart.utils import set_cart_cookie
+from ..checkout.utils import set_cart_cookie
 from ..core.utils import serialize_decimal
 from ..seo.schema.product import product_json_ld
 from .filters import ProductCategoryFilter, ProductCollectionFilter
-from .models import Category, Collection
+from .models import Category
 from .utils import (
     collections_visible_to_user, get_product_images, get_product_list_context,
     handle_cart_form, products_for_cart, products_with_details)
diff --git a/saleor/settings.py b/saleor/settings.py
index 19e26c74d58..b0deb678b0b 100644
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -123,7 +123,7 @@ def get_list(text):
     'django.contrib.messages.context_processors.messages',
     'django.template.context_processors.request',
     'saleor.core.context_processors.default_currency',
-    'saleor.cart.context_processors.cart_counter',
+    'saleor.checkout.context_processors.cart_counter',
     'saleor.core.context_processors.navigation',
     'saleor.core.context_processors.search_enabled',
     'saleor.site.context_processors.site',
@@ -187,7 +187,6 @@ def get_list(text):
     'saleor.account',
     'saleor.discount',
     'saleor.product',
-    'saleor.cart',
     'saleor.checkout',
     'saleor.core',
     'saleor.graphql',
diff --git a/saleor/urls.py b/saleor/urls.py
index dce06f973e5..670d8c1059f 100644
--- a/saleor/urls.py
+++ b/saleor/urls.py
@@ -8,8 +8,8 @@
 from graphene_django.views import GraphQLView
 
 from .account.urls import urlpatterns as account_urls
-from .cart.urls import urlpatterns as cart_urls
-from .checkout.urls import urlpatterns as checkout_urls
+from .checkout.urls import (
+    cart_urlpatterns as cart_urls, checkout_urlpatterns as checkout_urls)
 from .core.sitemaps import sitemaps
 from .core.urls import urlpatterns as core_urls
 from .dashboard.urls import urlpatterns as dashboard_urls
diff --git a/templates/base.html b/templates/base.html
index 9ab795e8aa6..f82bdc590ea 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -6,7 +6,7 @@
 {% load placeholder %}
 {% load translate_url from urls %}
 
-<html lang="{{ LANGUAGE_CODE }}" class="no-js" data-shipping-options-url="{% url 'cart:shipping-options' %}" data-cart-summary-url="{% url 'cart:cart-summary' %}">
+<html lang="{{ LANGUAGE_CODE }}" class="no-js" data-shipping-options-url="{% url 'cart:shipping-options' %}" data-cart-summary-url="{% url 'cart:summary' %}">
 <head>
   <title>{% block title %}{{ site.name }}{% endblock %}</title>
   {% block meta %}
diff --git a/templates/cart/_subtotal_table.html b/templates/checkout/_subtotal_table.html
similarity index 100%
rename from templates/cart/_subtotal_table.html
rename to templates/checkout/_subtotal_table.html
diff --git a/templates/checkout/details.html b/templates/checkout/details.html
index c77765e722e..283357fb1cc 100644
--- a/templates/checkout/details.html
+++ b/templates/checkout/details.html
@@ -33,7 +33,7 @@ <h1>{% trans "Checkout" context "Checkout title" %}</h1>
     <div class="col-md-5">
       <div class="checkout__review">
         <h3 class="checkout__review__tittle">{% trans "Order review" context "Order review title" %}</h3>
-        {% for line, line_total in checkout.lines %}
+        {% for line, line_total in cart_lines %}
           <div class="row">
             <div class="col-8">
               <p><strong>{{ line.variant.product }}</strong> x {{ line.quantity }}<br>
@@ -52,7 +52,7 @@ <h3 class="checkout__review__tittle">{% trans "Order review" context "Order revi
           </div>
           <div class="col-4">
             <p class="float-right">
-              {% price checkout.get_subtotal %}
+              {% price cart_subtotal %}
             </p>
           </div>
         </div>
@@ -62,44 +62,42 @@ <h3 class="checkout__review__tittle">{% trans "Order review" context "Order revi
           </div>
           <div class="col-4">
             <p class="float-right">
-              {% if checkout.shipping_method %}
-                {% price checkout.shipping_price %}
+              {% if cart.shipping_method %}
+                {% price cart_shipping_price %}
               {% else %}
                 &mdash;
               {% endif %}
             </p>
           </div>
         </div>
-        {% with total=checkout.get_total %}
-          {% if checkout.are_taxes_handled %}
-            <div class="row checkout__review__section">
-              <div class="col-8">
-                {% if site.settings.display_gross_prices %}
-                  <p>{% trans "Taxes (included)" context "Order review section title" %}</p>
-                {% else %}
-                  <p>{% trans "Taxes" context "Order review section title" %}</p>
-                {% endif %}
-              </div>
-              <div class="col-4">
-                <p class="float-right">{% price total.tax %}</p>
-              </div>
-            </div>
-          {% endif %}
-          <div class="row checkout__review__section">
-            <div class="col-12">
-              <h3>{% trans "Promo code" context "Order review section title" %}</h3>
-              {% include "checkout/snippets/voucher_form.html" with checkout=checkout form=voucher_form %}
-            </div>
-          </div>
+        {% if cart_are_taxes_handled %}
           <div class="row checkout__review__section">
             <div class="col-8">
-              <h3>{% trans "Total" context "Order review section title" %}</h3>
+              {% if site.settings.display_gross_prices %}
+                <p>{% trans "Taxes (included)" context "Order review section title" %}</p>
+              {% else %}
+                <p>{% trans "Taxes" context "Order review section title" %}</p>
+              {% endif %}
             </div>
             <div class="col-4">
-              <h3 class="float-right">{% price total.gross %}</h3>
+              <p class="float-right">{% price cart_total.tax %}</p>
             </div>
           </div>
-        {% endwith %}
+        {% endif %}
+        <div class="row checkout__review__section">
+          <div class="col-12">
+            <h3>{% trans "Promo code" context "Order review section title" %}</h3>
+            {% include "checkout/snippets/voucher_form.html" with checkout=checkout form=voucher_form %}
+          </div>
+        </div>
+        <div class="row checkout__review__section">
+          <div class="col-8">
+            <h3>{% trans "Total" context "Order review section title" %}</h3>
+          </div>
+          <div class="col-4">
+            <h3 class="float-right">{% price cart_total.gross %}</h3>
+          </div>
+        </div>
       </div>
     </div>
   </div>
diff --git a/templates/cart/index.html b/templates/checkout/index.html
similarity index 98%
rename from templates/cart/index.html
rename to templates/checkout/index.html
index 18f7ac6d081..97eb2e61d93 100644
--- a/templates/cart/index.html
+++ b/templates/checkout/index.html
@@ -73,7 +73,7 @@
         </div>
       {% endfor %}
       <div class="cart__subtotal">
-        {% include 'cart/_subtotal_table.html' %}
+        {% include 'checkout/_subtotal_table.html' %}
       </div>
       <div class="row">
         <div class="col-md-12">
diff --git a/templates/checkout/shipping_method.html b/templates/checkout/shipping_method.html
index c53cccfb3dc..c2a10ad78cd 100644
--- a/templates/checkout/shipping_method.html
+++ b/templates/checkout/shipping_method.html
@@ -5,7 +5,7 @@
 
 {% block forms %}
   <h3>{% trans "Shipping address" context "Checkout shipping address title" %}</h3>
-  {% format_address checkout.shipping_address %}
+  {% format_address cart.shipping_address %}
   <p><a href="{% url 'checkout:shipping-address' %}">{% trans "Select other address" %}</a></p>
   <hr>
   <form method="post" novalidate>
diff --git a/templates/checkout/snippets/voucher_form.html b/templates/checkout/snippets/voucher_form.html
index 2cd5565f3f9..5bbf90b69e1 100644
--- a/templates/checkout/snippets/voucher_form.html
+++ b/templates/checkout/snippets/voucher_form.html
@@ -3,12 +3,12 @@
 {% load discount_as_negative from voucher %}
 
 {% if form %}
-  {% if checkout.discount %}
+  {% if cart.discount_amount %}
     <form action="{% url "checkout:remove-voucher" %}?next={{ request.path }}"
           method="post"
           class="float-left input-btn">
       {% csrf_token %}
-      <span>{{ checkout.discount_name }}</span>
+      <span>{{ cart.discount_name }}</span>
       <button type="submit"
               class="btn btn-link btn-voucher-remove"
               aria-label="{% trans "Remove" context "Checkout voucher form action" %}">
@@ -16,7 +16,7 @@
       </button>
     </form>
     <div class="float-right">
-      {% discount_as_negative checkout.discount %}
+      {% discount_as_negative cart.discount_amount %}
     </div>
   {% else %}
     <form action="{{ request.path }}?next={{ request.path }}" method="post">
diff --git a/tests/conftest.py b/tests/conftest.py
index d93164e999d..bbfcef51123 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,9 +1,8 @@
-from decimal import Decimal
 from io import BytesIO
 from unittest.mock import MagicMock, Mock
 
 import pytest
-from django.contrib.auth.models import AnonymousUser, Group, Permission
+from django.contrib.auth.models import Group, Permission
 from django.contrib.sites.models import Site
 from django.core.files import File
 from django.core.files.uploadedfile import SimpleUploadedFile
@@ -16,9 +15,9 @@
 from prices import Money
 
 from saleor.account.models import Address, User
-from saleor.cart import utils
-from saleor.cart.models import Cart
-from saleor.checkout.core import Checkout
+from saleor.checkout import utils
+from saleor.checkout.models import Cart
+from saleor.checkout.utils import add_variant_to_cart
 from saleor.dashboard.order.utils import fulfill_order_line
 from saleor.discount.models import Sale, Voucher
 from saleor.menu.models import Menu, MenuItem
@@ -53,6 +52,16 @@ def cart(db):  # pylint: disable=W0613
     return Cart.objects.create()
 
 
+@pytest.fixture
+def cart_with_voucher(cart, product, voucher):
+    variant = product.variants.get()
+    add_variant_to_cart(cart, variant, 3)
+    cart.voucher_code = voucher.code
+    cart.discount_amount = Money('20.00', 'USD')
+    cart.save()
+    return cart
+
+
 @pytest.fixture
 def address(db):  # pylint: disable=W0613
     return Address.objects.create(
@@ -89,8 +98,7 @@ def request_cart(cart, monkeypatch):
 @pytest.fixture
 def request_cart_with_item(product, request_cart):
     variant = product.variants.get()
-    # Prepare some data
-    request_cart.add(variant)
+    add_variant_to_cart(request_cart, variant)
     return request_cart
 
 
@@ -349,19 +357,6 @@ def product_with_images(product_type, default_category):
     return product
 
 
-@pytest.fixture
-def checkout():
-    return Checkout(Mock(), AnonymousUser(), None, None, 'tracking_code')
-
-
-@pytest.fixture
-def checkout_with_items(request_cart_with_item, customer_user):
-    checkout = Checkout(
-        request_cart_with_item, customer_user, None, None, 'tracking_code')
-    checkout.shipping_address = customer_user.default_shipping_address
-    return checkout
-
-
 @pytest.fixture
 def voucher(db):  # pylint: disable=W0613
     return Voucher.objects.create(code='mirumee', discount_value=20)
@@ -369,18 +364,19 @@ def voucher(db):  # pylint: disable=W0613
 
 @pytest.fixture()
 def order_with_lines(
-        order, product_type, default_category, shipping_method, taxes):
+        order, product_type, default_category, shipping_method, vatlayer):
+    taxes = vatlayer
     product = Product.objects.create(
         name='Test product', price=Money('10.00', 'USD'),
         product_type=product_type, category=default_category)
     variant = ProductVariant.objects.create(
-        product=product, sku='SKU_A', cost_price=Money(1, 'USD'), quantity=0,
-        quantity_allocated=0)
+        product=product, sku='SKU_A', cost_price=Money(1, 'USD'), quantity=5,
+        quantity_allocated=3)
     order.lines.create(
         product_name=variant.display_product(),
         product_sku=variant.sku,
         is_shipping_required=variant.is_shipping_required(),
-        quantity=1,
+        quantity=3,
         variant=variant,
         unit_price=variant.get_price(taxes=taxes),
         tax_rate=taxes['standard']['value'])
@@ -389,13 +385,13 @@ def order_with_lines(
         name='Test product 2', price=Money('20.00', 'USD'),
         product_type=product_type, category=default_category)
     variant = ProductVariant.objects.create(
-        product=product, sku='SKU_B', cost_price=Money(2, 'USD'), quantity=0,
-        quantity_allocated=0)
+        product=product, sku='SKU_B', cost_price=Money(2, 'USD'), quantity=2,
+        quantity_allocated=2)
     order.lines.create(
         product_name=variant.display_product(),
         product_sku=variant.sku,
         is_shipping_required=variant.is_shipping_required(),
-        quantity=1,
+        quantity=2,
         variant=variant,
         unit_price=variant.get_price(taxes=taxes),
         tax_rate=taxes['standard']['value'])
@@ -413,51 +409,6 @@ def order_with_lines(
     return order
 
 
-@pytest.fixture()
-def order_with_lines_and_stock(
-        order, product_type, default_category, shipping_method):
-    product = Product.objects.create(
-        name='Test product', price=Money('10.00', 'USD'),
-        product_type=product_type, category=default_category)
-    variant = ProductVariant.objects.create(
-        product=product, sku='SKU_A', cost_price=Money(1, 'USD'), quantity=5,
-        quantity_allocated=3)
-    order.lines.create(
-        order=order,
-        product_name=product.name,
-        product_sku='SKU_A',
-        is_shipping_required=product.product_type.is_shipping_required,
-        quantity=3,
-        unit_price_net=Decimal('30.00'),
-        unit_price_gross=Decimal('30.00'),
-        variant=variant)
-    product = Product.objects.create(
-        name='Test product 2', price=Money('20.00', 'USD'),
-        product_type=product_type, category=default_category)
-    variant = ProductVariant.objects.create(
-        product=product, sku='SKU_B', cost_price=Money(2, 'USD'), quantity=2,
-        quantity_allocated=2)
-    order.lines.create(
-        order=order,
-        product_name=product.name,
-        product_sku='SKU_B',
-        is_shipping_required=product.product_type.is_shipping_required,
-        quantity=2,
-        unit_price_net=Decimal('20.00'),
-        unit_price_gross=Decimal('20.00'),
-        variant=variant)
-
-    order.shipping_address = order.billing_address.get_copy()
-    order.shipping_method_name = shipping_method.name
-    method = shipping_method.price_per_country.get()
-    order.shipping_method = method
-    order.shipping_price = method.get_total_price()
-    order.save()
-    recalculate_order(order)
-    order.refresh_from_db()
-    return order
-
-
 @pytest.fixture()
 def fulfilled_order(order_with_lines):
     order = order_with_lines
@@ -480,13 +431,6 @@ def draft_order(order_with_lines):
     return order_with_lines
 
 
-@pytest.fixture
-def draft_order_with_stock(order_with_lines_and_stock):
-    order_with_lines_and_stock.status = OrderStatus.DRAFT
-    order_with_lines_and_stock.save(update_fields=['status'])
-    return order_with_lines_and_stock
-
-
 @pytest.fixture()
 def payment_waiting(order_with_lines):
     return order_with_lines.payments.create(
diff --git a/tests/dashboard/test_order.py b/tests/dashboard/test_order.py
index ce99a873d6d..82f72b48856 100644
--- a/tests/dashboard/test_order.py
+++ b/tests/dashboard/test_order.py
@@ -5,8 +5,10 @@
 from django.urls import reverse
 from payments import PaymentStatus
 from prices import Money
+
 from tests.utils import get_form_errors, get_redirect_location
 
+from saleor.checkout import AddressType
 from saleor.core.utils.taxes import ZERO_MONEY, ZERO_TAXED_MONEY
 from saleor.dashboard.order.forms import ChangeQuantityForm, OrderNoteForm
 from saleor.dashboard.order.utils import (
@@ -15,9 +17,7 @@
 from saleor.discount.utils import increase_voucher_usage
 from saleor.order import OrderStatus
 from saleor.order.models import Order, OrderLine, OrderNote
-from saleor.order.utils import (
-    add_variant_to_existing_lines, add_variant_to_order,
-    change_order_line_quantity)
+from saleor.order.utils import add_variant_to_order, change_order_line_quantity
 from saleor.product.models import ProductVariant
 
 
@@ -416,15 +416,14 @@ def test_view_cancel_order_line(admin_client, draft_order):
 
 
 @pytest.mark.integration
-def test_view_change_order_line_quantity(admin_client, draft_order_with_stock):
-    order = draft_order_with_stock
-    lines_before_quantity_change = order.lines.all()
+def test_view_change_order_line_quantity(admin_client, draft_order):
+    lines_before_quantity_change = draft_order.lines.all()
     lines_before_quantity_change_count = lines_before_quantity_change.count()
     line = lines_before_quantity_change.first()
 
     url = reverse(
         'dashboard:orderline-change-quantity',
-        kwargs={'order_pk': order.pk, 'line_pk': line.pk})
+        kwargs={'order_pk': draft_order.pk, 'line_pk': line.pk})
     response = admin_client.get(url)
     assert response.status_code == 200
     response = admin_client.post(url, {'quantity': 2}, follow=True)
@@ -432,7 +431,7 @@ def test_view_change_order_line_quantity(admin_client, draft_order_with_stock):
     # check redirection
     assert redirect_status_code == 302
     assert redirected_to == reverse(
-        'dashboard:order-details', kwargs={'order_pk': order.id})
+        'dashboard:order-details', kwargs={'order_pk': draft_order.id})
     # success messages should appear after redirect
     assert response.context['messages']
     lines_after = Order.objects.get().lines.all()
@@ -460,8 +459,7 @@ def test_view_change_order_line_quantity_with_invalid_data(
 
 
 def test_dashboard_change_quantity_form(request_cart_with_item, order):
-    cart = request_cart_with_item
-    for line in cart.lines.all():
+    for line in request_cart_with_item:
         add_variant_to_order(order, line.variant, line.quantity)
     order_line = order.lines.get()
     quantity_before = order_line.variant.quantity_allocated
@@ -565,38 +563,17 @@ def test_view_fulfillment_packing_slips_without_shipping(
     assert response['content-type'] == 'application/pdf'
 
 
-def test_add_variant_to_existing_lines_one_line(
-        order_with_lines_and_stock):
-    order = order_with_lines_and_stock
-    lines = order.lines.filter(product_sku='SKU_A')
-    variant_lines_before = lines.count()
-    line = lines.first()
-    line_quantity_before = line.quantity
-    variant = ProductVariant.objects.get(sku='SKU_A')
-
-    quantity_added = 2
-    quantity_left = add_variant_to_existing_lines(
-        line.order, variant, quantity_added)
-
-    lines_after = order.lines.filter(product_sku='SKU_A').count()
-    line.refresh_from_db()
-    assert quantity_left == 0
-    assert lines_after == variant_lines_before
-    assert line.quantity == line_quantity_before + quantity_added
-
-
-def test_view_add_variant_to_order(
-        admin_client, order_with_lines_and_stock):
-    order = order_with_lines_and_stock
-    order.status = OrderStatus.DRAFT
-    order.save()
+def test_view_add_variant_to_order(admin_client, order_with_lines):
+    order_with_lines.status = OrderStatus.DRAFT
+    order_with_lines.save()
     variant = ProductVariant.objects.get(sku='SKU_A')
     line = OrderLine.objects.get(product_sku='SKU_A')
     line_quantity_before = line.quantity
 
     added_quantity = 2
     url = reverse(
-        'dashboard:add-variant-to-order', kwargs={'order_pk': order.pk})
+        'dashboard:add-variant-to-order',
+        kwargs={'order_pk': order_with_lines.pk})
     data = {'variant': variant.pk, 'quantity': added_quantity}
 
     response = admin_client.post(url, data)
@@ -604,7 +581,7 @@ def test_view_add_variant_to_order(
     line.refresh_from_db()
     assert response.status_code == 302
     assert get_redirect_location(response) == reverse(
-        'dashboard:order-details', kwargs={'order_pk': order.pk})
+        'dashboard:order-details', kwargs={'order_pk': order_with_lines.pk})
     assert line.quantity == line_quantity_before + added_quantity
 
 
@@ -973,7 +950,7 @@ def test_save_address_in_order_shipping_address(order, address):
     address.first_name = 'Jane'
     address.save()
 
-    save_address_in_order(order, address, 'shipping')
+    save_address_in_order(order, address, AddressType.SHIPPING)
 
     assert order.shipping_address == address
     assert order.shipping_address.pk == address.pk
@@ -984,7 +961,7 @@ def test_save_address_in_order_billing_address(order, address):
     address.first_name = 'Jane'
     address.save()
 
-    save_address_in_order(order, address, 'billing')
+    save_address_in_order(order, address, AddressType.BILLING)
 
     assert order.billing_address == address
     assert order.billing_address.pk == address.pk
diff --git a/tests/dashboard/test_taxes.py b/tests/dashboard/test_taxes.py
index 4d155ac1e4d..0661e838c1b 100644
--- a/tests/dashboard/test_taxes.py
+++ b/tests/dashboard/test_taxes.py
@@ -3,21 +3,13 @@
 from django_countries.fields import Country
 from django_prices_vatlayer.models import VAT
 from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange
-from tests.utils import get_redirect_location
 
 from saleor.core.utils import get_country_name_by_code
 from saleor.core.utils.taxes import (
     apply_tax_to_price, get_taxes_for_address, get_taxes_for_country)
 from saleor.dashboard.taxes.filters import get_country_choices_for_vat
 
-
-def compare_taxes(taxes_1, taxes_2):
-    assert len(taxes_1) == len(taxes_2)
-
-    for rate_name, tax in taxes_1.items():
-        value_1 = tax['value']
-        value_2 = taxes_2.get(rate_name)['value']
-        assert value_1 == value_2
+from ..utils import compare_taxes, get_redirect_location
 
 
 def test_view_taxes_list(admin_client, vatlayer):
diff --git a/tests/test_asynchronous_tasks.py b/tests/test_asynchronous_tasks.py
index 108a1da99a0..bd52c4a4331 100644
--- a/tests/test_asynchronous_tasks.py
+++ b/tests/test_asynchronous_tasks.py
@@ -6,9 +6,8 @@
 
 @pytest.mark.integration
 def test_email_sending_asynchronously(
-        transactional_db, celery_app, celery_worker,
-        order_with_lines_and_stock):
-    order = send_order_confirmation.delay(order_with_lines_and_stock.pk)
-    payment = send_payment_confirmation.delay(order_with_lines_and_stock.pk)
+        transactional_db, celery_app, celery_worker, order_with_lines):
+    order = send_order_confirmation.delay(order_with_lines.pk)
+    payment = send_payment_confirmation.delay(order_with_lines.pk)
     order.get()
     payment.get()
diff --git a/tests/test_cart.py b/tests/test_cart.py
index 3e71b56ade0..f9c092d6667 100644
--- a/tests/test_cart.py
+++ b/tests/test_cart.py
@@ -10,11 +10,14 @@
 from django.urls import reverse
 from prices import Money, TaxedMoney
 
-from saleor.cart import CartStatus, forms, utils
-from saleor.cart.context_processors import cart_counter
-from saleor.cart.models import Cart, find_open_cart_for_user
-from saleor.cart.views import update
+from saleor.checkout import forms, utils
+from saleor.checkout.context_processors import cart_counter
+from saleor.checkout.models import Cart
+from saleor.checkout.utils import (
+    add_variant_to_cart, change_cart_user, find_open_cart_for_user)
+from saleor.checkout.views import update_cart_line
 from saleor.core.exceptions import InsufficientStock
+from saleor.core.utils.taxes import ZERO_TAXED_MONEY
 from saleor.discount.models import Sale
 from saleor.shipping.utils import get_shipment_options
 
@@ -36,125 +39,79 @@ def create_request(user=None, token=None):
 
 
 @pytest.fixture()
-def opened_anonymous_cart(db):
-    return Cart.objects.get_or_create(user=None, status=CartStatus.OPEN)[0]
+def anonymous_cart(db):
+    return Cart.objects.get_or_create(user=None)[0]
 
 
 @pytest.fixture()
-def cancelled_anonymous_cart(db):
-    return Cart.objects.get_or_create(user=None, status=CartStatus.CANCELED)[0]
-
-
-@pytest.fixture()
-def opened_user_cart(customer_user):
-    return Cart.objects.get_or_create(
-        user=customer_user, status=CartStatus.OPEN)[0]
-
-
-@pytest.fixture()
-def cancelled_user_cart(customer_user):
-    return Cart.objects.get_or_create(
-        user=customer_user, status=CartStatus.CANCELED)[0]
+def user_cart(customer_user):
+    return Cart.objects.get_or_create(user=customer_user)[0]
 
 
 @pytest.fixture()
 def local_currency(monkeypatch):
     def side_effect(price, currency):
         return price
-    monkeypatch.setattr('saleor.cart.views.to_local_currency', side_effect)
+    monkeypatch.setattr('saleor.checkout.views.to_local_currency', side_effect)
 
 
-def test_get_or_create_anonymous_cart_from_token(
-        opened_anonymous_cart, cancelled_anonymous_cart, opened_user_cart,
-        cancelled_user_cart):
+def test_get_or_create_anonymous_cart_from_token(anonymous_cart, user_cart):
     queryset = Cart.objects.all()
     carts = list(queryset)
-    cart = utils.get_or_create_anonymous_cart_from_token(
-        opened_anonymous_cart.token)
-    assert Cart.objects.all().count() == 4
-    assert cart == opened_anonymous_cart
-
-    # test against getting closed carts
-    cart = utils.get_or_create_anonymous_cart_from_token(
-        cancelled_anonymous_cart.token)
-    assert Cart.objects.all().count() == 5
-    assert cart not in carts
-    assert cart.user is None
-    assert cart.status == CartStatus.OPEN
-    cart.delete()
+    cart = utils.get_or_create_anonymous_cart_from_token(anonymous_cart.token)
+    assert Cart.objects.all().count() == 2
+    assert cart == anonymous_cart
 
     # test against new token
     cart = utils.get_or_create_anonymous_cart_from_token(uuid4())
-    assert Cart.objects.all().count() == 5
+    assert Cart.objects.all().count() == 3
     assert cart not in carts
     assert cart.user is None
-    assert cart.status == CartStatus.OPEN
     cart.delete()
 
     # test against getting cart assigned to user
-    cart = utils.get_or_create_anonymous_cart_from_token(
-        opened_user_cart.token)
-    assert Cart.objects.all().count() == 5
+    cart = utils.get_or_create_anonymous_cart_from_token(user_cart.token)
+    assert Cart.objects.all().count() == 3
     assert cart not in carts
     assert cart.user is None
-    assert cart.status == CartStatus.OPEN
-    cart.delete()
 
 
 def test_get_or_create_user_cart(
-        customer_user, opened_anonymous_cart, cancelled_anonymous_cart,
-        opened_user_cart, cancelled_user_cart, admin_user):
+        customer_user, anonymous_cart, user_cart, admin_user):
     cart = utils.get_or_create_user_cart(customer_user)
-    assert Cart.objects.all().count() == 4
-    assert cart == opened_user_cart
+    assert Cart.objects.all().count() == 2
+    assert cart == user_cart
 
-    # test against getting closed carts
-    Cart.objects.create(user=admin_user, status=CartStatus.CANCELED)
+    # test against creating new carts
+    Cart.objects.create(user=admin_user)
     queryset = Cart.objects.all()
     carts = list(queryset)
     cart = utils.get_or_create_user_cart(admin_user)
-    assert Cart.objects.all().count() == 6
-    assert cart not in carts
-    assert cart.user is admin_user
-    assert cart.status == CartStatus.OPEN
-    cart.delete()
+    assert Cart.objects.all().count() == 3
+    assert cart in carts
+    assert cart.user == admin_user
 
 
-def test_get_anonymous_cart_from_token(
-        opened_anonymous_cart, cancelled_anonymous_cart, opened_user_cart,
-        cancelled_user_cart):
-    cart = utils.get_anonymous_cart_from_token(opened_anonymous_cart.token)
-    assert Cart.objects.all().count() == 4
-    assert cart == opened_anonymous_cart
-
-    # test against getting closed carts
-    cart = utils.get_anonymous_cart_from_token(cancelled_anonymous_cart.token)
-    assert Cart.objects.all().count() == 4
-    assert cart is None
+def test_get_anonymous_cart_from_token(anonymous_cart, user_cart):
+    cart = utils.get_anonymous_cart_from_token(anonymous_cart.token)
+    assert Cart.objects.all().count() == 2
+    assert cart == anonymous_cart
 
     # test against new token
     cart = utils.get_anonymous_cart_from_token(uuid4())
-    assert Cart.objects.all().count() == 4
+    assert Cart.objects.all().count() == 2
     assert cart is None
 
     # test against getting cart assigned to user
-    cart = utils.get_anonymous_cart_from_token(opened_user_cart.token)
-    assert Cart.objects.all().count() == 4
+    cart = utils.get_anonymous_cart_from_token(user_cart.token)
+    assert Cart.objects.all().count() == 2
     assert cart is None
 
 
-def test_get_user_cart(
-        opened_anonymous_cart, cancelled_anonymous_cart, opened_user_cart,
-        cancelled_user_cart, admin_user, customer_user):
+def test_get_user_cart(anonymous_cart, user_cart, admin_user, customer_user):
     cart = utils.get_user_cart(customer_user)
-    assert Cart.objects.all().count() == 4
-    assert cart == opened_user_cart
-
-    # test against getting closed carts
-    Cart.objects.create(user=admin_user, status=CartStatus.CANCELED)
-    cart = utils.get_user_cart(admin_user)
-    assert Cart.objects.all().count() == 5
-    assert cart is None
+    assert Cart.objects.all().count() == 2
+    assert cart == user_cart
 
 
 def test_get_or_create_cart_from_request(
@@ -167,9 +124,9 @@ def test_get_or_create_cart_from_request(
     mock_get_for_user = Mock(return_value=user_cart)
     mock_get_for_anonymous = Mock(return_value=anonymous_cart)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_or_create_user_cart', mock_get_for_user)
+        'saleor.checkout.utils.get_or_create_user_cart', mock_get_for_user)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_or_create_anonymous_cart_from_token',
+        'saleor.checkout.utils.get_or_create_anonymous_cart_from_token',
         mock_get_for_anonymous)
     returned_cart = utils.get_or_create_cart_from_request(request, queryset)
     mock_get_for_user.assert_called_once_with(customer_user, queryset)
@@ -190,14 +147,14 @@ def test_get_cart_from_request(
     user_cart = Cart(user=customer_user)
     mock_get_for_user = Mock(return_value=user_cart)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_user_cart', mock_get_for_user)
+        'saleor.checkout.utils.get_user_cart', mock_get_for_user)
     returned_cart = utils.get_cart_from_request(request, queryset)
     mock_get_for_user.assert_called_once_with(customer_user, queryset)
     assert returned_cart == user_cart
 
     mock_get_for_user = Mock(return_value=None)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_user_cart', mock_get_for_user)
+        'saleor.checkout.utils.get_user_cart', mock_get_for_user)
     returned_cart = utils.get_cart_from_request(request, queryset)
     mock_get_for_user.assert_called_once_with(customer_user, queryset)
     assert not Cart.objects.filter(token=returned_cart.token).exists()
@@ -205,7 +162,7 @@ def test_get_cart_from_request(
     anonymous_cart = Cart()
     mock_get_for_anonymous = Mock(return_value=anonymous_cart)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_anonymous_cart_from_token',
+        'saleor.checkout.utils.get_anonymous_cart_from_token',
         mock_get_for_anonymous)
     request = cart_request_factory(user=None, token=token)
     returned_cart = utils.get_cart_from_request(request, queryset)
@@ -214,15 +171,14 @@ def test_get_cart_from_request(
 
     mock_get_for_anonymous = Mock(return_value=None)
     monkeypatch.setattr(
-        'saleor.cart.utils.get_anonymous_cart_from_token',
+        'saleor.checkout.utils.get_anonymous_cart_from_token',
         mock_get_for_anonymous)
     returned_cart = utils.get_cart_from_request(request, queryset)
     assert not Cart.objects.filter(token=returned_cart.token).exists()
 
 
-def test_find_and_assign_anonymous_cart(
-        opened_anonymous_cart, customer_user, client):
-    cart_token = opened_anonymous_cart.token
+def test_find_and_assign_anonymous_cart(anonymous_cart, customer_user, client):
+    cart_token = anonymous_cart.token
     # Anonymous user has a cart with token stored in cookie
     value = signing.get_cookie_signer(salt=utils.COOKIE_NAME).sign(cart_token)
     client.cookies[utils.COOKIE_NAME] = value
@@ -233,8 +189,7 @@ def test_find_and_assign_anonymous_cart(
     assert response.context['user'] == customer_user
     # User should have only one cart, the same as he had previously in
     # anonymous session
-    authenticated_user_carts = customer_user.carts.filter(
-        status=CartStatus.OPEN)
+    authenticated_user_carts = customer_user.carts.all()
     assert authenticated_user_carts.count() == 1
     assert authenticated_user_carts[0].token == cart_token
 
@@ -245,8 +200,7 @@ def test_login_without_a_cart(customer_user, client):
         reverse('account:login'),
         {'username': customer_user.email, 'password': 'password'}, follow=True)
     assert response.context['user'] == customer_user
-    authenticated_user_carts = customer_user.carts.filter(
-        status=CartStatus.OPEN)
+    authenticated_user_carts = customer_user.carts.all()
     assert authenticated_user_carts.count() == 0
 
 
@@ -257,96 +211,77 @@ def test_login_with_incorrect_cookie_token(customer_user, client):
         reverse('account:login'),
         {'username': customer_user.email, 'password': 'password'}, follow=True)
     assert response.context['user'] == customer_user
-    authenticated_user_carts = customer_user.carts.filter(
-        status=CartStatus.OPEN)
+    authenticated_user_carts = customer_user.carts.all()
     assert authenticated_user_carts.count() == 0
 
 
 def test_find_and_assign_anonymous_cart_and_close_opened(
-        customer_user, opened_user_cart, opened_anonymous_cart,
-        cart_request_factory):
-    token = opened_anonymous_cart.token
-    token_user = opened_user_cart.token
+        customer_user, user_cart, anonymous_cart, cart_request_factory):
+    token = anonymous_cart.token
+    token_user = user_cart.token
     request = cart_request_factory(user=customer_user, token=token)
     utils.find_and_assign_anonymous_cart()(
         lambda request: Mock(delete_cookie=lambda name: None))(request)
     token_cart = Cart.objects.filter(token=token).first()
     user_cart = Cart.objects.filter(token=token_user).first()
+    assert token_cart is not None
     assert token_cart.user.pk == customer_user.pk
-    assert token_cart.status == CartStatus.OPEN
-    assert user_cart.status == CartStatus.CANCELED
+    assert not user_cart
 
 
 def test_adding_without_checking(cart, product):
     variant = product.variants.get()
-    cart.add(variant, 1000, check_quantity=False)
+    add_variant_to_cart(cart, variant, 1000, check_quantity=False)
     assert len(cart) == 1
 
 
 def test_adding_zero_quantity(cart, product):
     variant = product.variants.get()
-    cart.add(variant, 0)
+    add_variant_to_cart(cart, variant, 0)
     assert len(cart) == 0
 
 
 def test_adding_same_variant(cart, product, taxes):
     variant = product.variants.get()
-    cart.add(variant, 1)
-    cart.add(variant, 2)
+    add_variant_to_cart(cart, variant, 1)
+    add_variant_to_cart(cart, variant, 2)
     assert len(cart) == 1
-    assert cart.count() == {'total_quantity': 3}
+    assert cart.quantity == 3
     cart_total = TaxedMoney(net=Money('24.39', 'USD'), gross=Money(30, 'USD'))
-    assert cart.get_total(taxes=taxes) == cart_total
+    assert cart.get_subtotal(taxes=taxes) == cart_total
 
 
 def test_replacing_same_variant(cart, product):
     variant = product.variants.get()
-    cart.add(variant, 1, replace=True)
-    cart.add(variant, 2, replace=True)
+    add_variant_to_cart(cart, variant, 1, replace=True)
+    add_variant_to_cart(cart, variant, 2, replace=True)
     assert len(cart) == 1
-    assert cart.count() == {'total_quantity': 2}
+    assert cart.quantity == 2
 
 
 def test_adding_invalid_quantity(cart, product):
     variant = product.variants.get()
     with pytest.raises(ValueError):
-        cart.add(variant, -1)
+        add_variant_to_cart(cart, variant, -1)
 
 
-@pytest.mark.parametrize('create_line_data, get_line_data, lines_equal', [
-    (None, None, True),
-    ({'gift-wrap': True}, None, False),
-    ({'gift-wrap': True}, {'gift-wrap': True}, True)])
-def test_getting_line(
-        create_line_data, get_line_data, lines_equal, cart, product):
+def test_getting_line(cart, product):
     variant = product.variants.get()
     assert cart.get_line(variant) is None
-    line = cart.create_line(variant, 1, create_line_data)
-    fetched_line = cart.get_line(variant, data=get_line_data)
-    lines_are_equal = fetched_line == line
-    assert lines_equal is lines_are_equal
-
-
-def test_change_status(cart):
-    with pytest.raises(ValueError):
-        cart.change_status('spanish inquisition')
-
-    cart.change_status(CartStatus.OPEN)
-    assert cart.status == CartStatus.OPEN
-    cart.change_status(CartStatus.CANCELED)
-    assert cart.status == CartStatus.CANCELED
+    add_variant_to_cart(cart, variant)
+    assert cart.lines.get() == cart.get_line(variant)
 
 
 def test_shipping_detection(cart, product):
-    variant = product.variants.get()
     assert not cart.is_shipping_required()
-    cart.add(variant, 1, replace=True)
+    variant = product.variants.get()
+    add_variant_to_cart(cart, variant, replace=True)
     assert cart.is_shipping_required()
 
 
 def test_cart_counter(monkeypatch):
     monkeypatch.setattr(
-        'saleor.cart.context_processors.get_cart_from_request',
+        'saleor.checkout.context_processors.get_cart_from_request',
         Mock(return_value=Mock(quantity=4)))
     ret = cart_counter(Mock())
     assert ret == {'cart_counter': 4}
@@ -355,7 +290,8 @@ def test_cart_counter(monkeypatch):
 def test_get_product_variants_and_prices():
     variant = Mock(product_id=1, id=1, get_price=Mock(return_value=10))
     cart = MagicMock(spec=Cart)
-    cart.lines.all.return_value = [Mock(quantity=1, variant=variant)]
+    cart.__iter__ = Mock(
+        return_value=iter([Mock(quantity=1, variant=variant)]))
     variants = list(utils.get_product_variants_and_prices(cart, variant))
     assert variants == [(variant, 10)]
 
@@ -364,17 +300,17 @@ def test_contains_unavailable_variants():
     missing_variant = Mock(
         check_quantity=Mock(side_effect=InsufficientStock('')))
     cart = MagicMock()
-    cart.lines.all.return_value = [Mock(variant=missing_variant)]
+    cart.__iter__ = Mock(return_value=iter([Mock(variant=missing_variant)]))
     assert utils.contains_unavailable_variants(cart)
 
     variant = Mock(check_quantity=Mock())
-    cart.lines.all.return_value = [Mock(variant=variant)]
+    cart.__iter__ = Mock(return_value=iter([Mock(variant=variant)]))
     assert not utils.contains_unavailable_variants(cart)
 
 
 def test_remove_unavailable_variants(cart, product):
     variant = product.variants.get()
-    cart.add(variant, 1)
+    add_variant_to_cart(cart, variant)
     variant.quantity = 0
     variant.save()
     utils.remove_unavailable_variants(cart)
@@ -384,49 +320,47 @@ def test_remove_unavailable_variants(cart, product):
 def test_check_product_availability_and_warn(
         monkeypatch, cart, product):
     variant = product.variants.get()
-    cart.add(variant, 1)
+    add_variant_to_cart(cart, variant)
     monkeypatch.setattr(
         'django.contrib.messages.warning', Mock(warning=Mock()))
     monkeypatch.setattr(
-        'saleor.cart.utils.contains_unavailable_variants',
+        'saleor.checkout.utils.contains_unavailable_variants',
         Mock(return_value=False))
 
     utils.check_product_availability_and_warn(MagicMock(), cart)
     assert len(cart) == 1
 
     monkeypatch.setattr(
-        'saleor.cart.utils.contains_unavailable_variants',
+        'saleor.checkout.utils.contains_unavailable_variants',
         Mock(return_value=True))
     monkeypatch.setattr(
-        'saleor.cart.utils.remove_unavailable_variants',
-        lambda c: c.add(variant, 0, replace=True))
+        'saleor.checkout.utils.remove_unavailable_variants',
+        lambda c: add_variant_to_cart(cart, variant, 0, replace=True))
 
     utils.check_product_availability_and_warn(MagicMock(), cart)
     assert len(cart) == 0
 
 
-def test_add_to_cart_form():
-    cart_lines = []
-    cart = Mock(
-        add=lambda variant, quantity: cart_lines.append(variant),
-        get_line=Mock(return_value=Mock(quantity=1)))
+def test_add_to_cart_form(cart, product):
+    variant = product.variants.get()
+    add_variant_to_cart(cart, variant, 3)
     data = {'quantity': 1}
-    form = forms.AddToCartForm(data=data, cart=cart, product=Mock())
+    form = forms.AddToCartForm(data=data, cart=cart, product=product)
 
-    product_variant = Mock(check_quantity=Mock(return_value=None))
-    form.get_variant = Mock(return_value=product_variant)
+    form.get_variant = Mock(return_value=variant)
 
     assert form.is_valid()
     form.save()
-    assert cart_lines == [product_variant]
+    assert cart.lines.count() == 1
+    assert cart.lines.filter(variant=variant).exists()
 
     with pytest.raises(NotImplementedError):
         data = {'quantity': 1}
-        form = forms.AddToCartForm(data=data, cart=cart, product=Mock())
+        form = forms.AddToCartForm(data=data, cart=cart, product=product)
         form.is_valid()
     data = {}
 
-    form = forms.AddToCartForm(data=data, cart=cart, product=Mock())
+    form = forms.AddToCartForm(data=data, cart=cart, product=product)
     assert not form.is_valid()
 
 
@@ -458,7 +392,7 @@ def test_replace_cart_line_form(cart, product):
     initial_quantity = 1
     replaced_quantity = 4
 
-    cart.add(variant, initial_quantity)
+    add_variant_to_cart(cart, variant, initial_quantity)
     data = {'quantity': replaced_quantity}
     form = forms.ReplaceCartLineForm(data=data, cart=cart, variant=variant)
     assert form.is_valid()
@@ -472,7 +406,7 @@ def test_replace_cartline_form_when_insufficient_stock(
     initial_quantity = 1
     replaced_quantity = 4
 
-    cart.add(variant, initial_quantity)
+    add_variant_to_cart(cart, variant, initial_quantity)
     exception_mock = InsufficientStock(
         Mock(get_stock_quantity=Mock(return_value=2)))
     monkeypatch.setattr(
@@ -491,25 +425,21 @@ def test_view_empty_cart(client, request_cart):
     assert response.status_code == 200
 
 
-def test_view_cart_without_taxes(client, sale, product, request_cart):
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
+def test_view_cart_without_taxes(client, sale, request_cart_with_item):
     response = client.get(reverse('cart:index'))
     response_cart_line = response.context[0]['cart_lines'][0]
-    cart_line = request_cart.lines.first()
+    cart_line = request_cart_with_item.lines.first()
     assert not response_cart_line['get_total'].tax.amount
     assert not response_cart_line['get_total'] == cart_line.get_total()
     assert response.status_code == 200
 
 
 def test_view_cart_with_taxes(
-        settings, client, sale, product, request_cart, vatlayer):
+        settings, client, sale, request_cart_with_item, vatlayer):
     settings.DEFAULT_COUNTRY = 'PL'
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
     response = client.get(reverse('cart:index'))
     response_cart_line = response.context[0]['cart_lines'][0]
-    cart_line = request_cart.lines.first()
+    cart_line = request_cart_with_item.lines.first()
     assert response_cart_line['get_total'].tax.amount
     assert not response_cart_line['get_total'] == cart_line.get_total(
         taxes=vatlayer)
@@ -517,55 +447,48 @@ def test_view_cart_with_taxes(
 
 
 def test_view_update_cart_quantity(
-        client, local_currency, product, request_cart):
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
+        client, local_currency, request_cart_with_item):
+    variant = request_cart_with_item.lines.get().variant
     response = client.post(
         reverse('cart:update-line', kwargs={'variant_id': variant.pk}),
         data={'quantity': 3}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
     assert response.status_code == 200
-    assert request_cart.quantity == 3
+    assert request_cart_with_item.quantity == 3
 
 
 def test_view_update_cart_quantity_with_taxes(
-        client, local_currency, product, request_cart, vatlayer):
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
+        client, local_currency, request_cart_with_item, vatlayer):
+    variant = request_cart_with_item.lines.get().variant
     response = client.post(
         reverse('cart:update-line', kwargs={'variant_id': variant.id}),
         {'quantity': 3}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
     assert response.status_code == 200
-    assert request_cart.quantity == 3
+    assert request_cart_with_item.quantity == 3
 
 
-def test_view_invalid_update_cart(client, product, request_cart):
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
+def test_view_invalid_update_cart(client, request_cart_with_item):
+    variant = request_cart_with_item.lines.get().variant
     response = client.post(
         reverse('cart:update-line', kwargs={'variant_id': variant.pk}),
         data={}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
     resp_decoded = json.loads(response.content.decode('utf-8'))
     assert response.status_code == 400
     assert 'error' in resp_decoded.keys()
-    assert request_cart.quantity == 1
+    assert request_cart_with_item.quantity == 1
 
 
 def test_cart_page_without_openexchagerates(
-        client, product, request_cart, settings):
+        client, request_cart_with_item, settings):
     settings.OPENEXCHANGERATES_API_KEY = None
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
     response = client.get(reverse('cart:index'))
     context = response.context
     assert context['local_cart_total'] is None
 
 
 def test_cart_page_with_openexchagerates(
-        client, monkeypatch, product, request_cart, settings):
+        client, monkeypatch, request_cart_with_item, settings):
     settings.DEFAULT_COUNTRY = 'PL'
     settings.OPENEXCHANGERATES_API_KEY = 'fake-key'
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
     response = client.get(reverse('cart:index'))
     context = response.context
     assert context['local_cart_total'] is None
@@ -577,53 +500,43 @@ def test_cart_page_with_openexchagerates(
     assert context['local_cart_total'].currency == 'PLN'
 
 
-def test_cart_summary_page(settings, client, product, request_cart, vatlayer):
+def test_cart_summary_page(settings, client, request_cart_with_item, vatlayer):
     settings.DEFAULT_COUNTRY = 'PL'
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
-    response = client.get(reverse('cart:cart-summary'))
+    response = client.get(reverse('cart:summary'))
     assert response.status_code == 200
     content = response.context
-    assert content['quantity'] == request_cart.quantity
-    cart_total = request_cart.get_total(taxes=vatlayer)
+    assert content['quantity'] == request_cart_with_item.quantity
+    cart_total = request_cart_with_item.get_subtotal(taxes=vatlayer)
     assert content['total'] == cart_total
     assert len(content['lines']) == 1
     cart_line = content['lines'][0]
+    variant = request_cart_with_item.lines.get().variant
     assert cart_line['variant'] == variant.name
     assert cart_line['quantity'] == 1
 
 
 def test_cart_summary_page_empty_cart(client, request_cart):
-    response = client.get(reverse('cart:cart-summary'))
+    response = client.get(reverse('cart:summary'))
     assert response.status_code == 200
     data = response.context
     assert data['quantity'] == 0
 
 
 def test_cart_line_total_with_discount_and_taxes(
-        sale, request_cart, product, taxes):
+        sale, request_cart_with_item, taxes):
     sales = Sale.objects.all()
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
-    line = request_cart.lines.first()
+    line = request_cart_with_item.lines.first()
     assert line.get_total(discounts=sales, taxes=taxes) == TaxedMoney(
         net=Money('4.07', 'USD'), gross=Money('5.00', 'USD'))
 
 
-def test_cart_queryset():
-    canceled_cart = Cart.objects.create(status=CartStatus.CANCELED)
-    canceled = Cart.objects.canceled()
-    assert canceled.filter(pk=canceled_cart.pk).exists()
-
-
-def test_find_open_cart_for_user(customer_user, opened_user_cart):
-    assert find_open_cart_for_user(customer_user) == opened_user_cart
+def test_find_open_cart_for_user(customer_user, user_cart):
+    assert find_open_cart_for_user(customer_user) == user_cart
 
     cart = Cart.objects.create(user=customer_user)
 
     assert find_open_cart_for_user(customer_user) == cart
-    assert (
-        Cart.objects.get(pk=opened_user_cart.pk).status == CartStatus.CANCELED)
+    assert not Cart.objects.filter(pk=user_cart.pk).exists()
 
 
 def test_cart_repr():
@@ -636,39 +549,33 @@ def test_cart_repr():
 
 def test_cart_get_total_empty(db):
     cart = Cart.objects.create()
-
-    with pytest.raises(AttributeError):
-        cart.get_total()
+    assert cart.get_subtotal() == ZERO_TAXED_MONEY
 
 
 def test_cart_change_user(customer_user):
     cart1 = Cart.objects.create()
+    change_cart_user(cart1, customer_user)
 
-    cart1.change_user(customer_user)
     cart2 = Cart.objects.create()
+    change_cart_user(cart2, customer_user)
 
-    cart2.change_user(customer_user)
-
-    old_cart = Cart.objects.get(pk=cart1.pk)
-
-    assert old_cart.user == customer_user
-    assert old_cart.status == CartStatus.CANCELED
+    assert not Cart.objects.filter(pk=cart1.pk).exists()
 
 
 def test_cart_line_repr(product, request_cart_with_item):
     variant = product.variants.get()
     line = request_cart_with_item.lines.first()
-    assert repr(line) == 'CartLine(variant=%r, quantity=%r, data=%r)' % (
-        variant, line.quantity, line.data)
+    assert repr(line) == 'CartLine(variant=%r, quantity=%r)' % (
+        variant, line.quantity)
 
 
 def test_cart_line_state(product, request_cart_with_item):
     variant = product.variants.get()
     line = request_cart_with_item.lines.first()
 
-    assert line.__getstate__() == (variant, line.quantity, line.data)
+    assert line.__getstate__() == (variant, line.quantity)
 
-    line.__setstate__((variant, 2, line.data))
+    line.__setstate__((variant, 2))
 
     assert line.quantity == 2
 
@@ -685,7 +592,7 @@ def test_update_view_must_be_ajax(customer_user, rf):
     request = rf.post(reverse('home'))
     request.user = customer_user
     request.discounts = None
-    result = update(request, 1)
+    result = update_cart_line(request, 1)
     assert result.status_code == 302
 
 
@@ -723,3 +630,19 @@ def test_get_cart_data_no_shipping(request_cart_with_item, vatlayer):
     assert cart_total == TaxedMoney(
         net=Money('8.13', 'USD'), gross=Money(10, 'USD'))
     assert cart_data['total_with_shipping'].start == cart_total
+
+
+def test_cart_total_with_discount(request_cart_with_item, sale, vatlayer):
+    total = (
+        request_cart_with_item.get_total(discounts=(sale,), taxes=vatlayer))
+    assert total == TaxedMoney(
+        net=Money('4.07', 'USD'), gross=Money('5.00', 'USD'))
+
+
+def test_cart_taxes(request_cart_with_item, shipping_method, vatlayer):
+    cart = request_cart_with_item
+    cart.shipping_method = shipping_method.price_per_country.get()
+    cart.save()
+    taxed_price = TaxedMoney(net=Money('8.13', 'USD'), gross=Money(10, 'USD'))
+    assert cart.get_shipping_price(taxes=vatlayer) == taxed_price
+    assert cart.get_subtotal(taxes=vatlayer) == taxed_price
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
index 4e4f580a6da..424141ceefc 100644
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -1,275 +1,442 @@
-from unittest.mock import Mock
+import datetime
+from unittest.mock import Mock, patch
 
 import pytest
-from django.contrib.auth.models import AnonymousUser
 from django.urls import reverse
+from django_countries.fields import Country
+from freezegun import freeze_time
 from prices import Money, TaxedMoney
 
 from saleor.account.models import Address
 from saleor.checkout import views
-from saleor.checkout.core import STORAGE_SESSION_KEY, Checkout
-from saleor.checkout.forms import NoteForm
-from saleor.checkout.utils import get_voucher_discount_for_checkout
+from saleor.checkout.forms import CartVoucherForm
+from saleor.checkout.utils import (
+    add_variant_to_cart, change_billing_address_in_cart,
+    change_shipping_address_in_cart, create_order, get_cart_data_for_checkout,
+    get_taxes_for_cart, get_voucher_discount_for_cart, get_voucher_for_cart,
+    recalculate_cart_discount, remove_voucher_from_cart)
 from saleor.core.exceptions import InsufficientStock
+from saleor.core.utils.taxes import (
+    ZERO_MONEY, ZERO_TAXED_MONEY, get_taxes_for_country)
 from saleor.discount import DiscountValueType, VoucherType
-from saleor.discount.models import Voucher, NotApplicable
-from saleor.shipping.models import ShippingMethodCountry
-
-
-def test_checkout_version(checkout):
-    storage = checkout.for_storage()
-    assert storage['version'] == Checkout.VERSION
-
-
-@pytest.mark.parametrize('storage_data, expected_storage', [
-    (
-        {'version': Checkout.VERSION, 'new': 1},
-        {'version': Checkout.VERSION, 'new': 1}),
-    ({'version': 'wrong', 'new': 1}, {'version': Checkout.VERSION}),
-    ({'new': 1}, {'version': Checkout.VERSION}),
-    ({}, {'version': Checkout.VERSION}),
-    (None, {'version': Checkout.VERSION})])
-def test_checkout_version_with_from_storage(storage_data, expected_storage):
-    checkout = Checkout.from_storage(
-        storage_data, Mock(), AnonymousUser(), None, None, 'tracking_code')
-    storage = checkout.for_storage()
-    assert storage == expected_storage
-
-
-def test_checkout_clear_storage(checkout):
-    checkout.storage['new'] = 1
-    checkout.clear_storage()
-    assert checkout.storage is None
-    assert checkout.modified is True
-
-
-def test_checkout_is_shipping_required(checkout):
-    cart = Mock(is_shipping_required=Mock(return_value=True))
-    checkout.cart = cart
-    assert checkout.is_shipping_required is True
-
-
-@pytest.mark.parametrize('user, shipping', [
-    (Mock(default_shipping_address='user_shipping'), 'user_shipping'),
-    (AnonymousUser(), None)])
-def test_checkout_shipping_address_with_anonymous_user(
-        checkout, user, shipping):
-    checkout.user = user
-    assert checkout._shipping_address is None
-    assert checkout.shipping_address == shipping
-    assert checkout._shipping_address == shipping
-
-
-@pytest.mark.parametrize('address_objects, shipping', [
-    (Mock(get=Mock(return_value='shipping')), 'shipping'),
-    (Mock(get=Mock(side_effect=Address.DoesNotExist)), None)])
-def test_checkout_shipping_address_with_storage(
-        checkout, address_objects, shipping, monkeypatch):
+from saleor.discount.models import NotApplicable, Voucher
+
+from .utils import compare_taxes, get_redirect_location
+
+
+@pytest.mark.parametrize('cart_length, is_shipping_required, redirect_url', [
+    (0, True, reverse('checkout:index')),
+    (0, False, reverse('checkout:index')),
+    (1, True, reverse('checkout:shipping-address')),
+    (1, False, reverse('checkout:summary'))])
+def test_view_checkout_index(
+        monkeypatch, rf, cart_length, is_shipping_required, redirect_url):
+    cart = Mock(
+        __len__=Mock(return_value=cart_length),
+        is_shipping_required=Mock(return_value=is_shipping_required))
     monkeypatch.setattr(
-        'saleor.checkout.core.Address.objects', address_objects)
-    checkout.storage['shipping_address'] = {'id': 1}
-    assert checkout.shipping_address == shipping
-
-
-def test_checkout_shipping_address_setter(checkout):
-    address = Address(first_name='Jan', last_name='Kowalski')
-    assert checkout._shipping_address is None
-    checkout.shipping_address = address
-    assert checkout._shipping_address == address
-    assert checkout.storage['shipping_address'] == {
-        'city': '',
+        'saleor.checkout.utils.get_cart_from_request', lambda req, qs: cart)
+    url = reverse('checkout:index')
+    request = rf.get(url, follow=True)
+
+    response = views.checkout_index(request)
+
+    assert response.url == redirect_url
+
+
+def test_view_checkout_index_authorized_user(
+        authorized_client, customer_user, request_cart_with_item):
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
+    url = reverse('checkout:index')
+
+    response = authorized_client.get(url, follow=True)
+
+    redirect_url = reverse('checkout:shipping-address')
+    assert response.request['PATH_INFO'] == redirect_url
+
+
+def test_view_checkout_shipping_address(client, request_cart_with_item):
+    url = reverse('checkout:shipping-address')
+    data = {
+        'email': 'test@example.com',
+        'first_name': 'John',
+        'last_name': 'Doe',
+        'street_address_1': 'Aleje Jerozolimskie 2',
+        'street_address_2': '',
+        'city': 'Warszawa',
         'city_area': '',
-        'company_name': '',
-        'country': '',
         'country_area': '',
-        'first_name': 'Jan',
-        'id': None,
-        'last_name': 'Kowalski',
-        'phone': '',
-        'postal_code': '',
-        'street_address_1': '',
-        'street_address_2': ''}
-
-
-@pytest.mark.parametrize('shipping_address, shipping_method, value', [
-    (
-        Mock(country=Mock(code='PL')),
-        Mock(
-            country_code='PL',
-            __eq__=lambda n, o: n.country_code == o.country_code),
-        Mock(country_code='PL')),
-    (Mock(country=Mock(code='DE')), Mock(country_code='PL'), None),
-    (None, Mock(country_code='PL'), None)])
-def test_checkout_shipping_method(
-        checkout, shipping_address, shipping_method, value, monkeypatch):
-    queryset = Mock(get=Mock(return_value=shipping_method))
-    monkeypatch.setattr(Checkout, 'shipping_address', shipping_address)
-    monkeypatch.setattr(
-        'saleor.checkout.core.ShippingMethodCountry.objects', queryset)
-    checkout.storage['shipping_method_country_id'] = 1
-    assert checkout._shipping_method is None
-    assert checkout.shipping_method == value
-    assert checkout._shipping_method == value
+        'postal_code': '00-374',
+        'phone': '+48536984008',
+        'country': 'PL'}
 
+    response = client.get(url)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = client.post(url, data, follow=True)
+
+    redirect_url = reverse('checkout:shipping-method')
+    assert response.request['PATH_INFO'] == redirect_url
+    assert request_cart_with_item.email == 'test@example.com'
+
+
+def test_view_checkout_shipping_address_authorized_user(
+        authorized_client, customer_user, request_cart_with_item):
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
+    url = reverse('checkout:shipping-address')
+    data = {'address': customer_user.default_billing_address.pk}
+
+    response = authorized_client.post(url, data, follow=True)
+
+    redirect_url = reverse('checkout:shipping-method')
+    assert response.request['PATH_INFO'] == redirect_url
+    assert request_cart_with_item.email == customer_user.email
+
+
+def test_view_checkout_shipping_address_without_shipping(
+        request_cart, product_without_shipping, client):
+    variant = product_without_shipping.variants.get()
+    add_variant_to_cart(request_cart, variant)
+    url = reverse('checkout:shipping-address')
+
+    response = client.get(url)
+
+    assert response.status_code == 302
+    assert get_redirect_location(response) == reverse('checkout:summary')
+    assert not request_cart.email
+
+
+def test_view_checkout_shipping_method(
+        client, shipping_method, address, request_cart_with_item):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.save()
+    url = reverse('checkout:shipping-method')
+    data = {'shipping_method': shipping_method.price_per_country.first().pk}
+
+    response = client.get(url)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = client.post(url, data, follow=True)
+
+    redirect_url = reverse('checkout:summary')
+    assert response.request['PATH_INFO'] == redirect_url
+
+
+def test_view_checkout_shipping_method_authorized_user(
+        authorized_client, customer_user, shipping_method, address,
+        request_cart_with_item):
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.email = customer_user.email
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.save()
+    url = reverse('checkout:shipping-method')
+    data = {'shipping_method': shipping_method.price_per_country.first().pk}
+
+    response = authorized_client.get(url)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = authorized_client.post(url, data, follow=True)
+
+    redirect_url = reverse('checkout:summary')
+    assert response.request['PATH_INFO'] == redirect_url
+
+
+def test_view_checkout_shipping_method_without_shipping(
+        request_cart, product_without_shipping, client):
+    variant = product_without_shipping.variants.get()
+    add_variant_to_cart(request_cart, variant)
+    url = reverse('checkout:shipping-method')
+
+    response = client.get(url)
+
+    assert response.status_code == 302
+    assert get_redirect_location(response) == reverse('checkout:summary')
+
+
+def test_view_checkout_shipping_method_without_address(
+        request_cart_with_item, client):
+    url = reverse('checkout:shipping-method')
+
+    response = client.get(url)
+
+    assert response.status_code == 302
+    redirect_url = reverse('checkout:shipping-address')
+    assert get_redirect_location(response) == redirect_url
+
+
+@patch('saleor.checkout.views.summary.send_order_confirmation')
+def test_view_checkout_summary(
+        mock_send_confirmation, client, shipping_method, address,
+        request_cart_with_item):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+    url = reverse('checkout:summary')
+    data = {'address': 'shipping_address'}
+
+    response = client.get(url)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = client.post(url, data, follow=True)
+
+    order = response.context['order']
+    assert order.user_email == 'test@example.com'
+    redirect_url = reverse('order:payment', kwargs={'token': order.token})
+    assert response.request['PATH_INFO'] == redirect_url
+    mock_send_confirmation.delay.assert_called_once_with(order.pk)
+
+
+@patch('saleor.checkout.views.summary.send_order_confirmation')
+def test_view_checkout_summary_authorized_user(
+        mock_send_confirmation, authorized_client, customer_user,
+        shipping_method, address, request_cart_with_item):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.email = customer_user.email
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+    url = reverse('checkout:summary')
+    data = {'address': 'shipping_address'}
+
+    response = authorized_client.get(url)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = authorized_client.post(url, data, follow=True)
+
+    order = response.context['order']
+    assert order.user_email == customer_user.email
+    redirect_url = reverse('order:payment', kwargs={'token': order.token})
+    assert response.request['PATH_INFO'] == redirect_url
+    mock_send_confirmation.delay.assert_called_once_with(order.pk)
+
+
+@patch('saleor.checkout.views.summary.send_order_confirmation')
+def test_view_checkout_summary_save_language(
+        mock_send_confirmation, authorized_client, customer_user,
+        shipping_method, address, request_cart_with_item, settings):
+    settings.LANGUAGE_CODE = 'en'
+    user_language = 'fr'
+    authorized_client.cookies[settings.LANGUAGE_COOKIE_NAME] = user_language
+    url = reverse('set_language')
+    data = {'language': 'fr'}
+
+    authorized_client.post(url, data)
+
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.email = customer_user.email
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+    url = reverse('checkout:summary')
+    data = {'address': 'shipping_address'}
+
+    response = authorized_client.get(url, HTTP_ACCEPT_LANGUAGE=user_language)
+
+    assert response.request['PATH_INFO'] == url
+
+    response = authorized_client.post(
+        url, data, follow=True, HTTP_ACCEPT_LANGUAGE=user_language)
+
+    order = response.context['order']
+    assert order.user_email == customer_user.email
+    assert order.language_code == user_language
+    redirect_url = reverse('order:payment', kwargs={'token': order.token})
+    assert response.request['PATH_INFO'] == redirect_url
+    mock_send_confirmation.delay.assert_called_once_with(order.pk)
+
+
+def test_view_checkout_summary_without_address(request_cart_with_item, client):
+    url = reverse('checkout:summary')
+
+    response = client.get(url)
+
+    assert response.status_code == 302
+    redirect_url = reverse('checkout:shipping-method')
+    assert get_redirect_location(response) == redirect_url
+
+
+def test_view_checkout_summary_without_shipping_method(
+        request_cart_with_item, client):
+    url = reverse('checkout:summary')
+
+    response = client.get(url)
+
+    assert response.status_code == 302
+    redirect_url = reverse('checkout:shipping-method')
+    assert get_redirect_location(response) == redirect_url
+
+
+def test_view_checkout_summary_with_invalid_voucher(
+        client, request_cart_with_item, shipping_method, address, voucher):
+    voucher.usage_limit = 3
+    voucher.save()
+
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+
+    url = reverse('checkout:summary')
+    voucher_url = '{url}?next={url}'.format(url=url)
+    data = {'discount-voucher': voucher.code}
+
+    response = client.post(voucher_url, data, follow=True, HTTP_REFERER=url)
+
+    assert response.context['cart'].voucher_code == voucher.code
+
+    voucher.used = 3
+    voucher.save()
+
+    data = {'address': 'shipping_address'}
+    response = client.post(url, data, follow=True)
+    cart = response.context['cart']
+    assert not cart.voucher_code
+    assert not cart.discount_amount
+    assert not cart.discount_name
+
+    response = client.post(url, data, follow=True)
+    order = response.context['order']
+    assert not order.voucher
+    assert not order.discount_amount
+    assert not order.discount_name
+
+
+def test_view_checkout_summary_with_invalid_voucher_code(
+        client, request_cart_with_item, shipping_method, address):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+
+    url = reverse('checkout:summary')
+    voucher_url = '{url}?next={url}'.format(url=url)
+    data = {'discount-voucher': 'invalid-code'}
+
+    response = client.post(voucher_url, data, follow=True, HTTP_REFERER=url)
+
+    assert 'voucher' in response.context['voucher_form'].errors
+    assert response.context['cart'].voucher_code is None
+
+
+def test_view_checkout_summary_remove_voucher(
+        client, request_cart_with_item, shipping_method, voucher, address):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+
+    remove_voucher_url = reverse('checkout:summary')
+    voucher_url = '{url}?next={url}'.format(url=remove_voucher_url)
+    data = {'discount-voucher': voucher.code}
+
+    response = client.post(
+        voucher_url, data, follow=True, HTTP_REFERER=remove_voucher_url)
+
+    assert response.context['cart'].voucher_code == voucher.code
+
+    url = reverse('checkout:remove-voucher')
+
+    response = client.post(url, follow=True, HTTP_REFERER=remove_voucher_url)
+
+    assert not response.context['cart'].voucher_code
+
+
+def test_create_order_insufficient_stock(
+        request_cart, customer_user, product_without_shipping):
+    variant = product_without_shipping.variants.get()
+    add_variant_to_cart(request_cart, variant, 10, check_quantity=False)
+    request_cart.user = customer_user
+    request_cart.billing_address = customer_user.default_billing_address
+    request_cart.save()
 
-def test_checkout_shipping_does_not_exists(monkeypatch, checkout):
-    queryset = Mock(get=Mock(side_effect=ShippingMethodCountry.DoesNotExist))
-    monkeypatch.setattr(
-        'saleor.checkout.core.ShippingMethodCountry.objects', queryset)
-    checkout.storage['shipping_method_country_id'] = 1
-    assert checkout.shipping_method is None
-
-
-def test_checkout_shipping_method_setter(checkout):
-    assert checkout.modified is False
-    assert checkout._shipping_method is None
-    shipping_method = Mock(id=1)
-    checkout.shipping_method = shipping_method
-    assert checkout._shipping_method == shipping_method
-    assert checkout.modified is True
-    assert checkout.storage['shipping_method_country_id'] == 1
-
-
-@pytest.mark.parametrize('user, address', [
-    (AnonymousUser(), None),
-    (
-        Mock(
-            default_billing_address='billing_address',
-            addresses=Mock(
-                is_authenticated=Mock(return_value=True))),
-        'billing_address')])
-def test_checkout_billing_address(checkout, user, address):
-    checkout.user = user
-    assert checkout.billing_address == address
-
-
-@pytest.mark.parametrize('cart, status_code, url', [
-    (Mock(__len__=Mock(return_value=0)), 302, reverse('cart:index')),
-    (
-        Mock(
-            __len__=Mock(return_value=1),
-            is_shipping_required=Mock(return_value=True)),
-        302, reverse('checkout:shipping-address')),
-    (
-        Mock(
-            __len__=Mock(return_value=1),
-            is_shipping_required=Mock(return_value=False)),
-        302, reverse('checkout:summary')),
-    (
-        Mock(
-            __len__=Mock(return_value=0),
-            is_shipping_required=Mock(return_value=False)),
-        302, reverse('cart:index'))])
-def test_index_view(checkout, cart, status_code, url, rf, monkeypatch):
-    checkout.cart = cart
-    request = rf.get('checkout:index', follow=True)
-    request.user = checkout.user
-    request.session = {STORAGE_SESSION_KEY: checkout.for_storage()}
-    request.discounts = []
-    request.taxes = None
-    monkeypatch.setattr(
-        'saleor.cart.utils.get_cart_from_request', lambda req, qs: cart)
-    response = views.index_view(request)
-    assert response.status_code == status_code
-    assert response.url == url
-
-
-def test_checkout_discount(checkout_with_items, sale, vatlayer):
-    checkout_with_items.discounts = (sale,)
-    checkout_with_items.taxes = vatlayer
-    assert checkout_with_items.get_total() == TaxedMoney(
-        net=Money('4.07', 'USD'), gross=Money('5.00', 'USD'))
-
-
-def test_checkout_create_order_insufficient_stock(
-        checkout, request_cart, customer_user, product):
-    product_type = product.product_type
-    product_type.is_shipping_required = False
-    product_type.save()
-    variant = product.variants.get()
-    request_cart.add(variant, quantity=10, check_quantity=False)
-    checkout.cart = request_cart
-    checkout.user = customer_user
     with pytest.raises(InsufficientStock):
-        checkout.create_order()
-
-
-def test_checkout_taxes(checkout_with_items, shipping_method, vatlayer):
-    checkout_with_items.taxes = vatlayer
-    method = shipping_method.price_per_country.get()
-    checkout_with_items.shipping_method = method
-    assert checkout_with_items.shipping_price == TaxedMoney(
-        net=Money('8.13', 'USD'), gross=Money(10, 'USD'))
-    subtotal = checkout_with_items.cart.get_total(taxes=vatlayer)
-    assert checkout_with_items.get_subtotal() == subtotal
-
-
-@pytest.mark.parametrize('note_value', [
-    '',
-    '    ',
-    '   test_note  ',
-    'test_note'])
-def test_note_form(checkout, note_value):
-    form = NoteForm({'note': note_value}, checkout=checkout)
-    form.is_valid()
-    form.set_checkout_note()
-    assert checkout.note == note_value.strip()
-
-
-def test_note_in_created_order(checkout_with_items):
-    checkout_with_items.note = ''
-    order = checkout_with_items.create_order()
-    assert not order.notes.all()
-    checkout_with_items.note = 'test_note'
-    order = checkout_with_items.create_order()
+        create_order(
+            request_cart, 'tracking_code', discounts=None, taxes=None)
+
+
+def test_note_in_created_order(request_cart_with_item, address):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.note = 'test_note'
+    request_cart_with_item.save()
+    order = create_order(
+        request_cart_with_item, 'tracking_code', discounts=None, taxes=None)
     assert order.notes.filter(content='test_note').exists()
 
 
+def test_note_in_created_order_empty_note(request_cart_with_item, address):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.note = ''
+    request_cart_with_item.save()
+    order = create_order(
+        request_cart_with_item, 'tracking_code', discounts=None, taxes=None)
+    assert not order.notes.all()
+
+
 @pytest.mark.parametrize(
-    'total, discount_value, discount_type, limit, expected_value', [
+    'total, discount_value, discount_type, limit, discount_amount', [
         ('100', 10, DiscountValueType.FIXED, None, 10),
         ('100.05', 10, DiscountValueType.PERCENTAGE, 100, 10)])
-def test_value_voucher_checkout_discount(
-        settings, total, discount_value, discount_type, limit, expected_value):
+def test_get_discount_for_cart_value_voucher(
+        settings, total, discount_value, discount_type, limit,
+        discount_amount):
     voucher = Voucher(
-        code='unique', type=VoucherType.VALUE,
+        code='unique',
+        type=VoucherType.VALUE,
         discount_value_type=discount_type,
         discount_value=discount_value,
         limit=Money(limit, 'USD') if limit is not None else None)
     subtotal = TaxedMoney(net=Money(total, 'USD'), gross=Money(total, 'USD'))
-    checkout = Mock(get_subtotal=Mock(return_value=subtotal))
-    discount = get_voucher_discount_for_checkout(voucher, checkout)
-    assert discount == Money(expected_value, 'USD')
+    cart = Mock(get_subtotal=Mock(return_value=subtotal))
+    discount = get_voucher_discount_for_cart(voucher, cart)
+    assert discount == Money(discount_amount, 'USD')
 
 
-def test_value_voucher_checkout_discount_not_applicable(settings):
+def test_get_discount_for_cart_value_voucher_not_applicable(settings):
     voucher = Voucher(
-        code='unique', type=VoucherType.VALUE,
+        code='unique',
+        type=VoucherType.VALUE,
         discount_value_type=DiscountValueType.FIXED,
         discount_value=10,
         limit=Money(100, 'USD'))
     subtotal = TaxedMoney(net=Money(10, 'USD'), gross=Money(10, 'USD'))
-    checkout = Mock(get_subtotal=Mock(return_value=subtotal))
+    cart = Mock(get_subtotal=Mock(return_value=subtotal))
     with pytest.raises(NotApplicable) as e:
-        get_voucher_discount_for_checkout(voucher, checkout)
+        get_voucher_discount_for_cart(voucher, cart)
     assert e.value.limit == Money(100, 'USD')
 
 
 @pytest.mark.parametrize(
-    'shipping_cost, shipping_country_code, discount_value, discount_type, apply_to, expected_value', [  # noqa
+    'shipping_cost, shipping_country_code, discount_value, discount_type,'
+    'apply_to, expected_value', [
         (10, None, 50, DiscountValueType.PERCENTAGE, None, 5),
         (10, None, 20, DiscountValueType.FIXED, None, 10),
         (10, 'PL', 20, DiscountValueType.FIXED, '', 10),
         (5, 'PL', 5, DiscountValueType.FIXED, 'PL', 5)])
-def test_shipping_voucher_checkout_discount(
+def test_get_discount_for_cart_shipping_voucher(
         settings, shipping_cost, shipping_country_code, discount_value,
         discount_type, apply_to, expected_value):
     subtotal = TaxedMoney(net=Money(100, 'USD'), gross=Money(100, 'USD'))
     shipping_total = TaxedMoney(
         net=Money(shipping_cost, 'USD'), gross=Money(shipping_cost, 'USD'))
-    checkout = Mock(
+    cart = Mock(
         get_subtotal=Mock(return_value=subtotal),
-        is_shipping_required=True, shipping_method=Mock(
+        is_shipping_required=Mock(return_value=True),
+        shipping_method=Mock(
             price=Money(shipping_cost, 'USD'),
             country_code=shipping_country_code,
             get_total_price=Mock(return_value=shipping_total)))
@@ -279,12 +446,12 @@ def test_shipping_voucher_checkout_discount(
         discount_value=discount_value,
         apply_to=apply_to,
         limit=None)
-    discount = get_voucher_discount_for_checkout(voucher, checkout)
+    discount = get_voucher_discount_for_cart(voucher, cart)
     assert discount == Money(expected_value, 'USD')
 
 
 @pytest.mark.parametrize(
-    'is_shipping_required, shipping_method, discount_value, discount_type, '
+    'is_shipping_required, shipping_method, discount_value, discount_type,'
     'apply_to, limit, subtotal, error_msg', [
         (True, Mock(country_code='PL'), 10, DiscountValueType.FIXED,
          'US', None, Money(10, 'USD'),
@@ -298,14 +465,14 @@ def test_shipping_voucher_checkout_discount(
         (True, Mock(price=Money(10, 'USD')), 10,
          DiscountValueType.FIXED, None, 5, Money(2, 'USD'),
          'This offer is only valid for orders over $5.00.')])
-def test_shipping_voucher_checkout_discount_not_applicable(
+def test_get_discount_for_cart_shipping_voucher_not_applicable(
         settings, is_shipping_required, shipping_method, discount_value,
         discount_type, apply_to, limit, subtotal, error_msg):
     subtotal_price = TaxedMoney(net=subtotal, gross=subtotal)
-    checkout = Mock(
-        is_shipping_required=is_shipping_required,
-        shipping_method=shipping_method,
-        get_subtotal=Mock(return_value=subtotal_price))
+    cart = Mock(
+        get_subtotal=Mock(return_value=subtotal_price),
+        is_shipping_required=Mock(return_value=is_shipping_required),
+        shipping_method=shipping_method)
     voucher = Voucher(
         code='unique', type=VoucherType.SHIPPING,
         discount_value_type=discount_type,
@@ -313,11 +480,11 @@ def test_shipping_voucher_checkout_discount_not_applicable(
         limit=Money(limit, 'USD') if limit is not None else None,
         apply_to=apply_to)
     with pytest.raises(NotApplicable) as e:
-        get_voucher_discount_for_checkout(voucher, checkout)
+        get_voucher_discount_for_cart(voucher, cart)
     assert str(e.value) == error_msg
 
 
-def test_product_voucher_checkout_discount_not_applicable(
+def test_get_discount_for_cart_product_voucher_not_applicable(
         settings, monkeypatch):
     monkeypatch.setattr(
         'saleor.checkout.utils.get_product_variants_and_prices',
@@ -326,14 +493,14 @@ def test_product_voucher_checkout_discount_not_applicable(
         code='unique', type=VoucherType.PRODUCT,
         discount_value_type=DiscountValueType.FIXED,
         discount_value=10)
-    checkout = Mock(cart=Mock())
+    cart = Mock()
 
     with pytest.raises(NotApplicable) as e:
-        get_voucher_discount_for_checkout(voucher, checkout)
+        get_voucher_discount_for_cart(voucher, cart)
     assert str(e.value) == 'This offer is only valid for selected items.'
 
 
-def test_category_voucher_checkout_discount_not_applicable(
+def test_get_discount_for_cart_category_voucher_not_applicable(
         settings, monkeypatch):
     monkeypatch.setattr(
         'saleor.checkout.utils.get_category_variants_and_prices',
@@ -342,7 +509,245 @@ def test_category_voucher_checkout_discount_not_applicable(
         code='unique', type=VoucherType.CATEGORY,
         discount_value_type=DiscountValueType.FIXED,
         discount_value=10)
-    checkout = Mock(cart=Mock())
+    cart = Mock()
+
     with pytest.raises(NotApplicable) as e:
-        get_voucher_discount_for_checkout(voucher, checkout)
+        get_voucher_discount_for_cart(voucher, cart)
     assert str(e.value) == 'This offer is only valid for selected items.'
+
+
+def test_cart_voucher_form_invalid_voucher_code(
+        monkeypatch, request_cart_with_item):
+    form = CartVoucherForm(
+        {'voucher': 'invalid'}, instance=request_cart_with_item)
+    assert not form.is_valid()
+    assert 'voucher' in form.errors
+
+
+def test_cart_voucher_form_voucher_not_applicable(
+        voucher, request_cart_with_item):
+    voucher.limit = 200
+    voucher.save()
+    form = CartVoucherForm(
+        {'voucher': voucher.code}, instance=request_cart_with_item)
+    assert not form.is_valid()
+    assert 'voucher' in form.errors
+
+
+def test_cart_voucher_form_active_queryset_voucher_not_active(
+        voucher, request_cart_with_item):
+    assert Voucher.objects.count() == 1
+    voucher.start_date = datetime.date.today() + datetime.timedelta(days=1)
+    voucher.save()
+    form = CartVoucherForm(
+        {'voucher': voucher.code}, instance=request_cart_with_item)
+    qs = form.fields['voucher'].queryset
+    assert qs.count() == 0
+
+
+def test_cart_voucher_form_active_queryset_voucher_active(
+        voucher, request_cart_with_item):
+    assert Voucher.objects.count() == 1
+    voucher.start_date = datetime.date.today()
+    voucher.save()
+    form = CartVoucherForm(
+        {'voucher': voucher.code}, instance=request_cart_with_item)
+    qs = form.fields['voucher'].queryset
+    assert qs.count() == 1
+
+
+def test_cart_voucher_form_active_queryset_after_some_time(
+        voucher, request_cart_with_item):
+    assert Voucher.objects.count() == 1
+    voucher.start_date = datetime.date(year=2016, month=6, day=1)
+    voucher.end_date = datetime.date(year=2016, month=6, day=2)
+    voucher.save()
+
+    with freeze_time('2016-05-31'):
+        form = CartVoucherForm(
+            {'voucher': voucher.code}, instance=request_cart_with_item)
+        assert form.fields['voucher'].queryset.count() == 0
+
+    with freeze_time('2016-06-01'):
+        form = CartVoucherForm(
+            {'voucher': voucher.code}, instance=request_cart_with_item)
+        assert form.fields['voucher'].queryset.count() == 1
+
+    with freeze_time('2016-06-03'):
+        form = CartVoucherForm(
+            {'voucher': voucher.code}, instance=request_cart_with_item)
+        assert form.fields['voucher'].queryset.count() == 0
+
+
+def test_get_taxes_for_cart(cart, vatlayer):
+    taxes = get_taxes_for_cart(cart, vatlayer)
+    compare_taxes(taxes, vatlayer)
+
+
+def test_get_taxes_for_cart_with_shipping_address(cart, address, vatlayer):
+    address.country = 'DE'
+    address.save()
+    cart.shipping_address = address
+    cart.save()
+    taxes = get_taxes_for_cart(cart, vatlayer)
+    compare_taxes(taxes, get_taxes_for_country(Country('DE')))
+
+
+def test_get_taxes_for_cart_with_shipping_address_taxes_not_handled(
+        cart, settings, address, vatlayer):
+    settings.VATLAYER_ACCESS_KEY = ''
+    address.country = 'DE'
+    address.save()
+    cart.shipping_address = address
+    cart.save()
+    assert not get_taxes_for_cart(cart, None)
+
+
+def test_get_voucher_for_cart(cart_with_voucher, voucher):
+    cart_voucher = get_voucher_for_cart(cart_with_voucher)
+    assert cart_voucher == voucher
+
+
+def test_get_voucher_for_cart_expired_voucher(cart_with_voucher, voucher):
+    date_yesterday = datetime.date.today() - datetime.timedelta(days=1)
+    voucher.end_date = date_yesterday
+    voucher.save()
+    cart_voucher = get_voucher_for_cart(cart_with_voucher)
+    assert cart_voucher is None
+
+
+def test_get_voucher_for_cart_no_voucher_code(cart):
+    cart_voucher = get_voucher_for_cart(cart)
+    assert cart_voucher is None
+
+
+def test_remove_voucher_from_cart(cart_with_voucher):
+    cart = cart_with_voucher
+    remove_voucher_from_cart(cart)
+
+    assert not cart.voucher_code
+    assert not cart.discount_name
+    assert cart.discount_amount == ZERO_MONEY
+
+
+def test_recalculate_cart_discount(cart_with_voucher, voucher):
+    voucher.discount_value = 10
+    voucher.save()
+
+    recalculate_cart_discount(cart_with_voucher)
+
+    assert cart_with_voucher.discount_amount == Money('10.00', 'USD')
+
+
+def test_recalculate_cart_discount_voucher_not_applicable(
+        cart_with_voucher, voucher):
+    cart = cart_with_voucher
+    voucher.limit = 100
+    voucher.save()
+
+    recalculate_cart_discount(cart_with_voucher)
+
+    assert not cart.voucher_code
+    assert not cart.discount_name
+    assert cart.discount_amount == ZERO_MONEY
+
+
+def test_recalculate_cart_discount_expired_voucher(cart_with_voucher, voucher):
+    cart = cart_with_voucher
+    date_yesterday = datetime.date.today() - datetime.timedelta(days=1)
+    voucher.end_date = date_yesterday
+    voucher.save()
+
+    recalculate_cart_discount(cart_with_voucher)
+
+    assert not cart.voucher_code
+    assert not cart.discount_name
+    assert cart.discount_amount == ZERO_MONEY
+
+
+def test_get_cart_data_for_checkout(cart_with_voucher, vatlayer):
+    line_price = TaxedMoney(
+        net=Money('24.39', 'USD'), gross=Money('30.00', 'USD'))
+    expected_data = {
+        'cart': cart_with_voucher,
+        'cart_are_taxes_handled': True,
+        'cart_lines': [(cart_with_voucher.lines.first(), line_price)],
+        'cart_shipping_price': ZERO_TAXED_MONEY,
+        'cart_subtotal': line_price,
+        'cart_total': line_price - cart_with_voucher.discount_amount}
+
+    data = get_cart_data_for_checkout(
+        cart_with_voucher, discounts=None, taxes=vatlayer)
+
+    assert data == expected_data
+
+
+def test_change_address_in_cart(cart, address):
+    change_shipping_address_in_cart(cart, address)
+    change_billing_address_in_cart(cart, address)
+
+    cart.refresh_from_db()
+    assert cart.shipping_address == address
+    assert cart.billing_address == address
+
+
+def test_change_address_in_cart_to_none(cart, address):
+    cart.shipping_address = address
+    cart.billing_address = address.get_copy()
+    cart.save()
+
+    change_shipping_address_in_cart(cart, None)
+    change_billing_address_in_cart(cart, None)
+
+    cart.refresh_from_db()
+    assert cart.shipping_address is None
+    assert cart.billing_address is None
+
+
+def test_change_address_in_cart_to_same(cart, address):
+    cart.shipping_address = address
+    cart.billing_address = address.get_copy()
+    cart.save(update_fields=['shipping_address', 'billing_address'])
+    shipping_address_id = cart.shipping_address.id
+    billing_address_id = cart.billing_address.id
+
+    change_shipping_address_in_cart(cart, address)
+    change_billing_address_in_cart(cart, address)
+
+    cart.refresh_from_db()
+    assert cart.shipping_address.id == shipping_address_id
+    assert cart.billing_address.id == billing_address_id
+
+
+def test_change_address_in_cart_to_other(cart, address):
+    address_id = address.id
+    cart.shipping_address = address
+    cart.billing_address = address.get_copy()
+    cart.save(update_fields=['shipping_address', 'billing_address'])
+    other_address = Address.objects.create(country=Country('DE'))
+
+    change_shipping_address_in_cart(cart, other_address)
+    change_billing_address_in_cart(cart, other_address)
+
+    cart.refresh_from_db()
+    assert cart.shipping_address == other_address
+    assert cart.billing_address == other_address
+    assert not Address.objects.filter(id=address_id).exists()
+
+
+def test_change_address_in_cart_from_user_address_to_other(
+        cart, customer_user, address):
+    address_id = address.id
+    cart.user = customer_user
+    cart.shipping_address = address
+    cart.billing_address = address.get_copy()
+    cart.save(update_fields=['shipping_address', 'billing_address'])
+    other_address = Address.objects.create(country=Country('DE'))
+
+    change_shipping_address_in_cart(cart, other_address)
+    change_billing_address_in_cart(cart, other_address)
+
+    cart.refresh_from_db()
+    assert cart.shipping_address == other_address
+    assert cart.billing_address == other_address
+    assert Address.objects.filter(id=address_id).exists()
diff --git a/tests/test_checkout_integration.py b/tests/test_checkout_integration.py
deleted file mode 100644
index d4ceb9eeccb..00000000000
--- a/tests/test_checkout_integration.py
+++ /dev/null
@@ -1,421 +0,0 @@
-from unittest.mock import patch
-
-from django.urls import reverse
-from payments import FraudStatus, PaymentStatus
-
-from saleor.account.models import User
-
-from .utils import get_redirect_location
-
-
-@patch('saleor.checkout.views.summary.send_order_confirmation')
-def test_checkout_flow(
-        mock_send_confirmation, request_cart_with_item, client,
-        shipping_method):
-    # Enter checkout
-    checkout_index = client.get(reverse('checkout:index'), follow=True)
-    # Checkout index redirects directly to shipping address step
-    shipping_address = client.get(checkout_index.request['PATH_INFO'])
-
-    # Enter shipping address data
-    shipping_data = {
-        'email': 'test@example.com',
-        'first_name': 'John',
-        'last_name': 'Doe',
-        'street_address_1': 'Aleje Jerozolimskie 2',
-        'street_address_2': '',
-        'city': 'Warszawa',
-        'city_area': '',
-        'country_area': '',
-        'postal_code': '00-374',
-        'phone': '+48536984008',
-        'country': 'PL'}
-    shipping_response = client.post(
-        shipping_address.request['PATH_INFO'], data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_page = client.get(shipping_response.request['PATH_INFO'])
-
-    # Redirect to summary after shipping method selection
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    address_data = {'address': 'shipping_address'}
-    summary_response = client.post(
-        shipping_method_response.request['PATH_INFO'], data=address_data,
-        follow=True)
-
-    # After summary step, order is created and it waits for payment
-    order = summary_response.context['order']
-
-    # Select payment method
-    payment_page = client.post(
-        summary_response.request['PATH_INFO'], data={'method': 'default'},
-        follow=True)
-    assert len(payment_page.redirect_chain) == 1
-    assert payment_page.status_code == 200
-    # Go to payment details page, enter payment data
-    payment_page_url = payment_page.redirect_chain[0][0]
-    payment_data = {
-        'status': PaymentStatus.PREAUTH,
-        'fraud_status': FraudStatus.UNKNOWN,
-        'gateway_response': '3ds-disabled',
-        'verification_result': 'waiting'}
-    payment_response = client.post(payment_page_url, data=payment_data)
-    assert payment_response.status_code == 302
-    payment_redirect = reverse(
-        'order:payment-success', kwargs={'token': order.token})
-    assert get_redirect_location(payment_response) == payment_redirect
-    success_response = client.post(payment_redirect, data={'status': 'ok'})
-    assert success_response.status_code == 302
-    order_password = reverse(
-        'order:checkout-success', kwargs={'token': order.token})
-    assert get_redirect_location(success_response) == order_password
-    mock_send_confirmation.delay.assert_called_once_with(order.pk)
-
-
-def test_checkout_flow_authenticated_user(
-        authorized_client, request_cart_with_item, customer_user,
-        shipping_method):
-    # Prepare some data
-    request_cart_with_item.user = customer_user
-    request_cart_with_item.save()
-
-    # Enter checkout
-    # Checkout index redirects directly to shipping address step
-    shipping_address = authorized_client.get(
-        reverse('checkout:index'), follow=True)
-
-    # Enter shipping address data
-    shipping_data = {'address': customer_user.default_billing_address.pk}
-    shipping_method_page = authorized_client.post(
-        shipping_address.request['PATH_INFO'], data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = authorized_client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    payment_method_data = {'address': 'shipping_address'}
-    payment_method_page = authorized_client.post(
-        shipping_method_response.request['PATH_INFO'],
-        data=payment_method_data, follow=True)
-
-    # After summary step, order is created and it waits for payment
-    order = payment_method_page.context['order']
-
-    # Select payment method
-    payment_page = authorized_client.post(
-        payment_method_page.request['PATH_INFO'], data={'method': 'default'},
-        follow=True)
-
-    # Go to payment details page, enter payment data
-    payment_data = {
-        'status': PaymentStatus.PREAUTH,
-        'fraud_status': FraudStatus.UNKNOWN,
-        'gateway_response': '3ds-disabled',
-        'verification_result': 'waiting'}
-    payment_response = authorized_client.post(
-        payment_page.request['PATH_INFO'], data=payment_data)
-
-    assert payment_response.status_code == 302
-    order_password = reverse(
-        'order:payment-success', kwargs={'token': order.token})
-    assert get_redirect_location(payment_response) == order_password
-
-    # Assert that payment object was created and contains correct data
-    payment = order.payments.all()[0]
-    assert payment.total == order.total.gross.amount
-    assert payment.tax == order.total.tax.amount
-    assert payment.currency == order.total.currency
-    assert payment.delivery == order.shipping_price.net.amount
-    assert len(payment.get_purchased_items()) == len(order.lines.all())
-
-
-def test_address_without_shipping(request_cart_with_item, client, monkeypatch):
-    monkeypatch.setattr(
-        'saleor.checkout.core.Checkout.is_shipping_required', False)
-
-    response = client.get(reverse('checkout:shipping-address'))
-    assert response.status_code == 302
-    assert get_redirect_location(response) == reverse('checkout:summary')
-
-
-def test_shipping_method_without_shipping(
-        request_cart_with_item, client, monkeypatch):
-    monkeypatch.setattr('saleor.checkout.core.Checkout.is_shipping_required',
-                        False)
-
-    response = client.get(reverse('checkout:shipping-method'))
-    assert response.status_code == 302
-    assert get_redirect_location(response) == reverse('checkout:summary')
-
-
-def test_shipping_method_without_address(request_cart_with_item, client):
-    response = client.get(reverse('checkout:shipping-method'))
-    assert response.status_code == 302
-    assert (
-        get_redirect_location(response) ==
-        reverse('checkout:shipping-address'))
-
-
-def test_summary_without_address(request_cart_with_item, client):
-    response = client.get(reverse('checkout:summary'))
-    assert response.status_code == 302
-    assert (
-        get_redirect_location(response) == reverse('checkout:shipping-method'))
-
-
-def test_summary_without_shipping_method(
-        request_cart_with_item, client, monkeypatch):
-    # address test return true
-    monkeypatch.setattr(
-        'saleor.checkout.core.Checkout.email', True)
-
-    response = client.get(reverse('checkout:summary'))
-    assert response.status_code == 302
-    assert (
-        get_redirect_location(response) == reverse('checkout:shipping-method'))
-
-
-def test_email_is_saved_in_order(
-        authorized_client, customer_user, request_cart_with_item,
-        shipping_method):
-    # Prepare some data
-    request_cart_with_item.user = customer_user
-    request_cart_with_item.save()
-
-    # Enter checkout
-    # Checkout index redirects directly to shipping address step
-    shipping_address = authorized_client.get(
-        reverse('checkout:index'), follow=True)
-
-    # Enter shipping address data
-    shipping_data = {'address': customer_user.default_billing_address.pk}
-    shipping_method_page = authorized_client.post(
-        shipping_address.request['PATH_INFO'], data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = authorized_client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    payment_method_data = {'address': 'shipping_address'}
-    payment_method_page = authorized_client.post(
-        shipping_method_response.request['PATH_INFO'],
-        data=payment_method_data, follow=True)
-
-    # After summary step, order is created and it waits for payment
-    order = payment_method_page.context['order']
-    assert order.user_email == customer_user.email
-
-
-def test_voucher_invalid(
-        client, request_cart_with_item, shipping_method, voucher):
-    # related issues: #549 #544
-    voucher.usage_limit = 3
-    voucher.save()
-    # Enter checkout
-    checkout_index = client.get(reverse('checkout:index'), follow=True)
-    # Checkout index redirects directly to shipping address step
-    shipping_address = client.get(checkout_index.request['PATH_INFO'])
-
-    # Enter shipping address data
-    shipping_data = {
-        'email': 'test@example.com',
-        'first_name': 'John',
-        'last_name': 'Doe',
-        'street_address_1': 'Aleje Jerozolimskie 2',
-        'street_address_2': '',
-        'city': 'Warszawa',
-        'city_area': '',
-        'country_area': '',
-        'postal_code': '00-374',
-        'country': 'PL'}
-    shipping_response = client.post(shipping_address.request['PATH_INFO'],
-                                    data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_page = client.get(shipping_response.request['PATH_INFO'])
-
-    # Redirect to summary after shipping method selection
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    url = shipping_method_response.request['PATH_INFO']
-    discount_data = {'discount-voucher': voucher.code}
-    voucher_response = client.post(
-        '{url}?next={url}'.format(url=url), follow=True, data=discount_data,
-        HTTP_REFERER=url)
-    assert voucher_response.context['checkout'].voucher_code == voucher.code
-    voucher.used = 3
-    voucher.save()
-    address_data = {'address': 'shipping_address'}
-    assert url == reverse('checkout:summary')
-    summary_response = client.post(url, data=address_data, follow=True)
-    assert summary_response.context['checkout'].voucher_code is None
-
-    summary_response = client.post(url, data=address_data, follow=True)
-    assert summary_response.context['order'].voucher is None
-
-
-def test_voucher_code_invalid(
-        client, request_cart_with_item, shipping_method):
-    # Enter checkout
-    checkout_index = client.get(reverse('checkout:index'), follow=True)
-    # Checkout index redirects directly to shipping address step
-    shipping_address = client.get(checkout_index.request['PATH_INFO'])
-
-    # Enter shipping address data
-    shipping_data = {
-        'email': 'test@example.com',
-        'first_name': 'John',
-        'last_name': 'Doe',
-        'street_address_1': 'Aleje Jerozolimskie 2',
-        'street_address_2': '',
-        'city': 'Warszawa',
-        'city_area': '',
-        'country_area': '',
-        'postal_code': '00-374',
-        'country': 'PL'}
-    shipping_response = client.post(shipping_address.request['PATH_INFO'],
-                                    data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_page = client.get(shipping_response.request['PATH_INFO'])
-
-    # Redirect to summary after shipping method selection
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    url = shipping_method_response.request['PATH_INFO']
-    discount_data = {'discount-voucher': 'invalid-code'}
-    voucher_response = client.post(
-        '{url}?next={url}'.format(url=url), follow=True, data=discount_data,
-        HTTP_REFERER=url)
-    assert voucher_response.status_code == 200
-    assert 'voucher' in voucher_response.context['voucher_form'].errors
-    assert voucher_response.context['checkout'].voucher_code is None
-
-
-def test_remove_voucher(
-        client, request_cart_with_item, shipping_method, voucher):
-    # Enter checkout
-    checkout_index = client.get(reverse('checkout:index'), follow=True)
-    # Checkout index redirects directly to shipping address step
-    shipping_address = client.get(checkout_index.request['PATH_INFO'])
-
-    # Enter shipping address data
-    shipping_data = {
-        'email': 'test@example.com',
-        'first_name': 'John',
-        'last_name': 'Doe',
-        'street_address_1': 'Aleje Jerozolimskie 2',
-        'street_address_2': '',
-        'city': 'Warszawa',
-        'city_area': '',
-        'country_area': '',
-        'postal_code': '00-374',
-        'country': 'PL'}
-    shipping_response = client.post(shipping_address.request['PATH_INFO'],
-                                    data=shipping_data, follow=True)
-
-    # Select shipping method
-    shipping_method_page = client.get(shipping_response.request['PATH_INFO'])
-
-    # Redirect to summary after shipping method selection
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    url = shipping_method_response.request['PATH_INFO']
-    discount_data = {'discount-voucher': voucher.code}
-    voucher_response = client.post(
-        '{url}?next={url}'.format(url=url), follow=True, data=discount_data,
-        HTTP_REFERER=url)
-    assert voucher_response.context['checkout'].voucher_code is not None
-    # Remove voucher from checkout
-    voucher_response = client.post(
-        reverse('checkout:remove-voucher'), follow=True, HTTP_REFERER=url)
-    assert voucher_response.status_code == 200
-    assert voucher_response.context['checkout'].voucher_code is None
-
-
-def test_language_is_saved_in_order(
-        authorized_client, customer_user, request_cart_with_item, settings,
-        shipping_method):
-    # Prepare some data
-    settings.LANGUAGE_CODE = 'en'
-    user_language = 'fr'
-    request_cart_with_item.user = customer_user
-    request_cart_with_item.save()
-
-    # Set user's language to fr
-    authorized_client.cookies[settings.LANGUAGE_COOKIE_NAME] = user_language
-    authorized_client.post(reverse('set_language'), data={'language': 'fr'})
-    # Enter checkout
-    # Checkout index redirects directly to shipping address step
-    shipping_address = authorized_client.get(
-        reverse('checkout:index'), follow=True,
-        HTTP_ACCEPT_LANGUAGE=user_language)
-
-    # Enter shipping address data
-    shipping_data = {'address': customer_user.default_billing_address.pk}
-    shipping_method_page = authorized_client.post(
-        shipping_address.request['PATH_INFO'], data=shipping_data, follow=True,
-        HTTP_ACCEPT_LANGUAGE=user_language)
-
-    # Select shipping method
-    shipping_method_data = {
-        'method': shipping_method.price_per_country.first().pk}
-    shipping_method_response = authorized_client.post(
-        shipping_method_page.request['PATH_INFO'], data=shipping_method_data,
-        follow=True, HTTP_ACCEPT_LANGUAGE=user_language)
-
-    # Summary page asks for Billing address, default is the same as shipping
-    payment_method_data = {'address': 'shipping_address'}
-    payment_method_page = authorized_client.post(
-        shipping_method_response.request['PATH_INFO'],
-        data=payment_method_data, follow=True,
-        HTTP_ACCEPT_LANGUAGE=user_language)
-
-    # After summary step, order is created and it waits for payment
-    order = payment_method_page.context['order']
-    assert order.language_code == user_language
-
-
-def test_create_user_after_order(order, client):
-    order.user_email = 'hello@mirumee.com'
-    order.save()
-    assert not User.objects.filter(email='hello@mirumee.com').exists()
-    data = {'password': 'password'}
-    url = reverse('order:checkout-success', kwargs={'token': order.token})
-    response = client.post(url, data=data)
-    redirect_location = get_redirect_location(response)
-    detail_url = reverse('order:details', kwargs={'token': order.token})
-    assert redirect_location == detail_url
-    user = User.objects.filter(email='hello@mirumee.com')
-    assert user.exists()
-    user = user.first()
-    assert user.orders.filter(token=order.token).exists()
diff --git a/tests/test_discounts.py b/tests/test_discounts.py
index c918e0ac37c..44c034a809e 100644
--- a/tests/test_discounts.py
+++ b/tests/test_discounts.py
@@ -2,13 +2,11 @@
 from unittest.mock import Mock
 
 import pytest
-from freezegun import freeze_time
 from prices import Money, TaxedMoney
 
-from saleor.checkout.utils import get_voucher_discount_for_checkout
+from saleor.checkout.utils import get_voucher_discount_for_cart
 from saleor.discount import (
     DiscountValueType, VoucherApplyToProduct, VoucherType)
-from saleor.discount.forms import CheckoutDiscountForm
 from saleor.discount.models import NotApplicable, Sale, Voucher
 from saleor.discount.utils import (
     decrease_voucher_usage, get_product_discount_on_sale,
@@ -68,66 +66,6 @@ def test_voucher_queryset_active(voucher):
     assert len(active_vouchers) == 0
 
 
-def test_checkout_discount_form_invalid_voucher_code(monkeypatch):
-    checkout = Mock(cart=Mock())
-    form = CheckoutDiscountForm({'voucher': 'invalid'}, checkout=checkout)
-    assert not form.is_valid()
-    assert 'voucher' in form.errors
-
-
-def test_checkout_discount_form_not_applicable_voucher(monkeypatch, voucher):
-    checkout = Mock(cart=Mock())
-    form = CheckoutDiscountForm({'voucher': voucher.code}, checkout=checkout)
-    monkeypatch.setattr(
-        'saleor.discount.forms.get_voucher_discount_for_checkout',
-        Mock(side_effect=NotApplicable('Not applicable')))
-    assert not form.is_valid()
-    assert 'voucher' in form.errors
-
-
-def test_checkout_discount_form_active_queryset_voucher_not_active(voucher):
-    assert Voucher.objects.count() == 1
-    checkout = Mock(cart=Mock())
-    voucher.start_date = date.today() + timedelta(days=1)
-    voucher.save()
-    form = CheckoutDiscountForm({'voucher': voucher.code}, checkout=checkout)
-    qs = form.fields['voucher'].queryset
-    assert qs.count() == 0
-
-
-def test_checkout_discount_form_active_queryset_voucher_active(voucher):
-    assert Voucher.objects.count() == 1
-    checkout = Mock(cart=Mock())
-    voucher.start_date = date.today()
-    voucher.save()
-    form = CheckoutDiscountForm({'voucher': voucher.code}, checkout=checkout)
-    qs = form.fields['voucher'].queryset
-    assert qs.count() == 1
-
-
-def test_checkout_discount_form_active_queryset_after_some_time(voucher):
-    assert Voucher.objects.count() == 1
-    checkout = Mock(cart=Mock())
-    voucher.start_date = date(year=2016, month=6, day=1)
-    voucher.end_date = date(year=2016, month=6, day=2)
-    voucher.save()
-
-    with freeze_time('2016-05-31'):
-        form = CheckoutDiscountForm(
-            {'voucher': voucher.code}, checkout=checkout)
-        assert form.fields['voucher'].queryset.count() == 0
-
-    with freeze_time('2016-06-01'):
-        form = CheckoutDiscountForm(
-            {'voucher': voucher.code}, checkout=checkout)
-        assert form.fields['voucher'].queryset.count() == 1
-
-    with freeze_time('2016-06-03'):
-        form = CheckoutDiscountForm(
-            {'voucher': voucher.code}, checkout=checkout)
-        assert form.fields['voucher'].queryset.count() == 0
-
-
 @pytest.mark.parametrize(
     'prices, discount_value, discount_type, apply_to, expected_value', [
         (
@@ -170,7 +108,7 @@ def test_products_voucher_checkout_discount_not(
         discount_value=discount_value,
         apply_to=apply_to)
     checkout = Mock(cart=Mock())
-    discount = get_voucher_discount_for_checkout(voucher, checkout)
+    discount = get_voucher_discount_for_cart(voucher, checkout)
     assert discount == Money(expected_value, 'USD')
 
 
diff --git a/tests/test_order.py b/tests/test_order.py
index 1572b83fec0..85b41f3ca0b 100644
--- a/tests/test_order.py
+++ b/tests/test_order.py
@@ -3,10 +3,13 @@
 
 from django.urls import reverse
 from django_countries.fields import Country
-from payments import PaymentStatus
+from payments import FraudStatus, PaymentStatus
 from prices import Money, TaxedMoney
+
 from tests.utils import get_redirect_location
 
+from saleor.account.models import User
+from saleor.checkout.utils import create_order
 from saleor.core.utils.taxes import (
     DEFAULT_TAX_RATE_NAME, get_tax_rate_by_name, get_taxes_for_country)
 from saleor.order import FulfillmentStatus, OrderStatus, models
@@ -79,41 +82,38 @@ def test_add_variant_to_order_adds_line_for_new_variant(
 
 
 def test_add_variant_to_order_allocates_stock_for_new_variant(
-        order_with_lines_and_stock, product):
-    order = order_with_lines_and_stock
+        order_with_lines, product):
     variant = product.variants.get()
     stock_before = variant.quantity_allocated
 
-    add_variant_to_order(order, variant, 1)
+    add_variant_to_order(order_with_lines, variant, 1)
 
     variant.refresh_from_db()
     assert variant.quantity_allocated == stock_before + 1
 
 
 def test_add_variant_to_order_edits_line_for_existing_variant(
-        order_with_lines_and_stock):
-    order = order_with_lines_and_stock
-    existing_line = order.lines.first()
+        order_with_lines):
+    existing_line = order_with_lines.lines.first()
     variant = existing_line.variant
-    lines_before = order.lines.count()
+    lines_before = order_with_lines.lines.count()
     line_quantity_before = existing_line.quantity
 
-    add_variant_to_order(order, variant, 1)
+    add_variant_to_order(order_with_lines, variant, 1)
 
     existing_line.refresh_from_db()
-    assert order.lines.count() == lines_before
+    assert order_with_lines.lines.count() == lines_before
     assert existing_line.product_sku == variant.sku
     assert existing_line.quantity == line_quantity_before + 1
 
 
 def test_add_variant_to_order_allocates_stock_for_existing_variant(
-        order_with_lines_and_stock):
-    order = order_with_lines_and_stock
-    existing_line = order.lines.first()
+        order_with_lines):
+    existing_line = order_with_lines.lines.first()
     variant = existing_line.variant
     stock_before = variant.quantity_allocated
 
-    add_variant_to_order(order, variant, 1)
+    add_variant_to_order(order_with_lines, variant, 1)
 
     variant.refresh_from_db()
     assert variant.quantity_allocated == stock_before + 1
@@ -163,14 +163,6 @@ def test_add_note_to_order(order_with_lines):
     assert order.notes.first().content == 'test_note'
 
 
-def test_create_order_history(order_with_lines):
-    order = order_with_lines
-    order.history.create(content='test_entry', user=order.user)
-    history_entry = models.OrderHistoryEntry.objects.get(order=order)
-    assert history_entry == order.history.first()
-    assert history_entry.content == 'test_entry'
-
-
 def test_restock_order_lines(order_with_lines):
     order = order_with_lines
     line_1 = order.lines.first()
@@ -286,8 +278,7 @@ def test_order_queryset_confirmed(draft_order):
         Order.objects.create(status=OrderStatus.UNFULFILLED),
         Order.objects.create(status=OrderStatus.PARTIALLY_FULFILLED),
         Order.objects.create(status=OrderStatus.FULFILLED),
-        Order.objects.create(status=OrderStatus.CANCELED)
-    ]
+        Order.objects.create(status=OrderStatus.CANCELED)]
 
     confirmed_orders = Order.objects.confirmed()
 
@@ -370,7 +361,7 @@ def test_ajax_order_shipping_methods_list_different_country(
     assert resp_decoded == {'results': shipping_methods_list}
 
 
-def test_update_order_prices(order_with_lines, vatlayer):
+def test_update_order_prices(order_with_lines):
     taxes = get_taxes_for_country(Country('DE'))
     address = order_with_lines.shipping_address
     address.country = 'DE'
@@ -389,5 +380,75 @@ def test_update_order_prices(order_with_lines, vatlayer):
     assert line_1.unit_price == price_1
     assert line_2.unit_price == price_2
     assert order_with_lines.shipping_price == shipping_price
-    total = price_1 + price_2 + shipping_price
+    total = (
+        line_1.quantity * price_1 + line_2.quantity * price_2 + shipping_price)
     assert order_with_lines.total == total
+
+
+def test_order_payment_flow(
+        request_cart_with_item, client, address, shipping_method):
+    request_cart_with_item.shipping_address = address
+    request_cart_with_item.billing_address = address.get_copy()
+    request_cart_with_item.email = 'test@example.com'
+    request_cart_with_item.shipping_method = (
+        shipping_method.price_per_country.first())
+    request_cart_with_item.save()
+
+    order = create_order(
+        request_cart_with_item, 'tracking_code', discounts=None, taxes=None)
+
+    # Select payment method
+    url = reverse('order:payment', kwargs={'token': order.token})
+    data = {'method': 'default'}
+    response = client.post(url, data, follow=True)
+
+    assert len(response.redirect_chain) == 1
+    assert response.status_code == 200
+    redirect_url = reverse(
+        'order:payment', kwargs={'token': order.token, 'variant': 'default'})
+    assert response.request['PATH_INFO'] == redirect_url
+
+    # Go to payment details page, enter payment data
+    data = {
+        'status': PaymentStatus.PREAUTH,
+        'fraud_status': FraudStatus.UNKNOWN,
+        'gateway_response': '3ds-disabled',
+        'verification_result': 'waiting'}
+
+    response = client.post(redirect_url, data)
+
+    assert response.status_code == 302
+    redirect_url = reverse(
+        'order:payment-success', kwargs={'token': order.token})
+    assert get_redirect_location(response) == redirect_url
+
+    # Complete payment, go to checkout success page
+    data = {'status': 'ok'}
+    response = client.post(redirect_url, data)
+    assert response.status_code == 302
+    redirect_url = reverse(
+        'order:checkout-success', kwargs={'token': order.token})
+    assert get_redirect_location(response) == redirect_url
+
+    # Assert that payment object was created and contains correct data
+    payment = order.payments.all()[0]
+    assert payment.total == order.total.gross.amount
+    assert payment.tax == order.total.tax.amount
+    assert payment.currency == order.total.currency
+    assert payment.delivery == order.shipping_price.net.amount
+    assert len(payment.get_purchased_items()) == len(order.lines.all())
+
+
+def test_create_user_after_order(order, client):
+    order.user_email = 'hello@mirumee.com'
+    order.save()
+    url = reverse('order:checkout-success', kwargs={'token': order.token})
+    data = {'password': 'password'}
+
+    response = client.post(url, data)
+
+    redirect_url = reverse('order:details', kwargs={'token': order.token})
+    assert get_redirect_location(response) == redirect_url
+    user = User.objects.filter(email='hello@mirumee.com').first()
+    assert user is not None
+    assert user.orders.filter(token=order.token).exists()
diff --git a/tests/test_product.py b/tests/test_product.py
index 7e1d0ac988f..1bcf370b6ee 100644
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -6,8 +6,9 @@
 from django.urls import reverse
 from prices import Money, TaxedMoney, TaxedMoneyRange
 
-from saleor.cart import CartStatus, utils
-from saleor.cart.models import Cart
+from saleor.checkout import utils
+from saleor.checkout.models import Cart
+from saleor.checkout.utils import add_variant_to_cart
 from saleor.discount.models import Sale
 from saleor.product import ProductAvailabilityStatus, models
 from saleor.product.thumbnails import create_product_thumbnails
@@ -147,7 +148,7 @@ def test_render_product_detail_with_taxes(client, product, vatlayer):
 
 def test_view_invalid_add_to_cart(client, product, request_cart):
     variant = product.variants.get()
-    request_cart.add(variant, 2)
+    add_variant_to_cart(request_cart, variant, 2)
     response = client.post(
         reverse(
             'product:add-to-cart',
@@ -159,9 +160,8 @@ def test_view_invalid_add_to_cart(client, product, request_cart):
     assert request_cart.quantity == 2
 
 
-def test_view_add_to_cart(client, product, request_cart):
-    variant = product.variants.get()
-    request_cart.add(variant, 1)
+def test_view_add_to_cart(client, product, request_cart_with_item):
+    variant = request_cart_with_item.lines.get().variant
     response = client.post(
         reverse(
             'product:add-to-cart',
@@ -169,118 +169,118 @@ def test_view_add_to_cart(client, product, request_cart):
                     'product_id': product.pk}),
         {'quantity': 1, 'variant': variant.pk})
     assert response.status_code == 302
-    assert request_cart.quantity == 1
+    assert request_cart_with_item.quantity == 1
 
 
 def test_adding_to_cart_with_current_user_token(
-        admin_user, admin_client, product):
-    client = admin_client
+        customer_user, authorized_client, product, request_cart_with_item):
     key = utils.COOKIE_NAME
-    cart = Cart.objects.create(user=admin_user)
-    variant = product.variants.first()
-    cart.add(variant, 1)
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
 
-    response = client.get(reverse('cart:index'))
-    utils.set_cart_cookie(cart, response)
-    client.cookies[key] = response.cookies[key]
+    response = authorized_client.get(reverse('cart:index'))
 
-    client.post(
-        reverse('product:add-to-cart',
-                kwargs={'slug': product.get_slug(),
-                        'product_id': product.pk}),
-        {'quantity': 1, 'variant': variant.pk})
+    utils.set_cart_cookie(request_cart_with_item, response)
+    authorized_client.cookies[key] = response.cookies[key]
+    variant = request_cart_with_item.lines.first().variant
+    url = reverse(
+        'product:add-to-cart',
+        kwargs={'slug': product.get_slug(), 'product_id': product.pk})
+    data = {'quantity': 1, 'variant': variant.pk}
+
+    authorized_client.post(url, data)
 
     assert Cart.objects.count() == 1
-    assert Cart.objects.get(user=admin_user).pk == cart.pk
+    assert Cart.objects.get(user=customer_user).pk == request_cart_with_item.pk
 
 
 def test_adding_to_cart_with_another_user_token(
-        admin_user, admin_client, product, customer_user):
+        admin_user, admin_client, product, customer_user, request_cart_with_item):
     client = admin_client
     key = utils.COOKIE_NAME
-    cart = Cart.objects.create(user=customer_user)
-    variant = product.variants.first()
-    cart.add(variant, 1)
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
 
     response = client.get(reverse('cart:index'))
-    utils.set_cart_cookie(cart, response)
+
+    utils.set_cart_cookie(request_cart_with_item, response)
     client.cookies[key] = response.cookies[key]
+    variant = request_cart_with_item.lines.first().variant
+    url = reverse(
+        'product:add-to-cart',
+        kwargs={'slug': product.get_slug(), 'product_id': product.pk})
+    data = {'quantity': 1, 'variant': variant.pk}
 
-    client.post(
-        reverse('product:add-to-cart',
-                kwargs={'slug': product.get_slug(),
-                        'product_id': product.pk}),
-        {'quantity': 1, 'variant': variant.pk})
+    client.post(url, data)
 
     assert Cart.objects.count() == 2
-    assert Cart.objects.get(user=admin_user).pk != cart.pk
+    assert Cart.objects.get(user=admin_user).pk != request_cart_with_item.pk
 
 
 def test_anonymous_adding_to_cart_with_another_user_token(
-        client, product, customer_user):
+        client, product, customer_user, request_cart_with_item):
     key = utils.COOKIE_NAME
-    cart = Cart.objects.create(user=customer_user)
-    variant = product.variants.first()
-    cart.add(variant, 1)
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
 
     response = client.get(reverse('cart:index'))
-    utils.set_cart_cookie(cart, response)
+
+    utils.set_cart_cookie(request_cart_with_item, response)
     client.cookies[key] = response.cookies[key]
+    variant = product.variants.get()
+    url = reverse(
+        'product:add-to-cart',
+        kwargs={'slug': product.get_slug(), 'product_id': product.pk})
+    data = {'quantity': 1, 'variant': variant.pk}
 
-    client.post(
-        reverse('product:add-to-cart',
-                kwargs={'slug': product.get_slug(),
-                        'product_id': product.pk}),
-        {'quantity': 1, 'variant': variant.pk})
+    client.post(url, data)
 
     assert Cart.objects.count() == 2
-    assert Cart.objects.get(user=None).pk != cart.pk
+    assert Cart.objects.get(user=None).pk != request_cart_with_item.pk
 
 
 def test_adding_to_cart_with_deleted_cart_token(
-        admin_user, admin_client, product):
-    client = admin_client
+        customer_user, authorized_client, product, request_cart_with_item):
     key = utils.COOKIE_NAME
-    cart = Cart.objects.create(user=admin_user)
-    old_token = cart.token
-    variant = product.variants.first()
-    cart.add(variant, 1)
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
+    old_token = request_cart_with_item.token
 
-    response = client.get(reverse('cart:index'))
-    utils.set_cart_cookie(cart, response)
-    client.cookies[key] = response.cookies[key]
-    cart.delete()
+    response = authorized_client.get(reverse('cart:index'))
 
-    client.post(
-        reverse('product:add-to-cart',
-                kwargs={'slug': product.get_slug(),
-                        'product_id': product.pk}),
-        {'quantity': 1, 'variant': variant.pk})
+    utils.set_cart_cookie(request_cart_with_item, response)
+    authorized_client.cookies[key] = response.cookies[key]
+    request_cart_with_item.delete()
+    variant = product.variants.get()
+    url = reverse(
+        'product:add-to-cart',
+        kwargs={'slug': product.get_slug(), 'product_id': product.pk})
+    data = {'quantity': 1, 'variant': variant.pk}
+
+    authorized_client.post(url, data)
 
     assert Cart.objects.count() == 1
     assert not Cart.objects.filter(token=old_token).exists()
 
 
 def test_adding_to_cart_with_closed_cart_token(
-        admin_user, admin_client, product):
-    client = admin_client
+        customer_user, authorized_client, product, request_cart_with_item):
     key = utils.COOKIE_NAME
-    cart = Cart.objects.create(user=admin_user)
-    variant = product.variants.first()
-    cart.add(variant, 1)
+    request_cart_with_item.user = customer_user
+    request_cart_with_item.save()
 
-    response = client.get(reverse('cart:index'))
-    utils.set_cart_cookie(cart, response)
-    client.cookies[key] = response.cookies[key]
+    response = authorized_client.get(reverse('cart:index'))
+    utils.set_cart_cookie(request_cart_with_item, response)
+    authorized_client.cookies[key] = response.cookies[key]
+    variant = product.variants.get()
+    url = reverse(
+        'product:add-to-cart',
+        kwargs={'slug': product.get_slug(), 'product_id': product.pk})
+    data = {'quantity': 1, 'variant': variant.pk}
 
-    client.post(
-        reverse('product:add-to-cart',
-                kwargs={'slug': product.get_slug(),
-                        'product_id': product.pk}),
-        {'quantity': 1, 'variant': variant.pk})
+    authorized_client.post(url, data)
 
-    assert Cart.objects.filter(
-        user=admin_user, status=CartStatus.OPEN).count() == 1
+    assert customer_user.carts.count() == 1
 
 
 def test_product_filter_before_filtering(
@@ -288,9 +288,13 @@ def test_product_filter_before_filtering(
     products = models.Product.objects.all().filter(
         category__name=default_category).order_by('-price')
     url = reverse(
-        'product:category', kwargs={'path': default_category.slug,
-                                    'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
+
     response = authorized_client.get(url)
+
     assert list(products) == list(response.context['filter_set'].qs)
 
 
@@ -301,20 +305,28 @@ def test_product_filter_product_exists(authorized_client, product,
         .filter(category__name=default_category)
         .order_by('-price'))
     url = reverse(
-        'product:category', kwargs={
-            'path': default_category.slug, 'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
     data = {'price_0': [''], 'price_1': ['20']}
+
     response = authorized_client.get(url, data)
+
     assert list(response.context['filter_set'].qs) == list(products)
 
 
 def test_product_filter_product_does_not_exist(
         authorized_client, product, default_category):
     url = reverse(
-        'product:category', kwargs={
-            'path': default_category.slug, 'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
     data = {'price_0': ['20'], 'price_1': ['']}
+
     response = authorized_client.get(url, data)
+
     assert not list(response.context['filter_set'].qs)
 
 
@@ -325,9 +337,13 @@ def test_product_filter_form(authorized_client, product,
         .filter(category__name=default_category)
         .order_by('-price'))
     url = reverse(
-        'product:category', kwargs={
-            'path': default_category.slug, 'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
+
     response = authorized_client.get(url)
+
     assert 'price' in response.context['filter_set'].form.fields.keys()
     assert 'sort_by' in response.context['filter_set'].form.fields.keys()
     assert list(response.context['filter_set'].qs) == list(products)
@@ -340,20 +356,28 @@ def test_product_filter_sorted_by_price_descending(
         .filter(category__name=default_category, is_published=True)
         .order_by('-price'))
     url = reverse(
-        'product:category', kwargs={
-            'path': default_category.slug, 'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
     data = {'sort_by': '-price'}
+
     response = authorized_client.get(url, data)
+
     assert list(response.context['filter_set'].qs) == list(products)
 
 
 def test_product_filter_sorted_by_wrong_parameter(
         authorized_client, product, default_category):
     url = reverse(
-        'product:category', kwargs={
-            'path': default_category.slug, 'category_id': default_category.pk})
+        'product:category',
+        kwargs={
+            'path': default_category.slug,
+            'category_id': default_category.pk})
     data = {'sort_by': 'aaa'}
+
     response = authorized_client.get(url, data)
+
     assert not list(response.context['filter_set'].qs)
 
 
@@ -368,22 +392,22 @@ def test_view_ajax_available_variants_list(admin_client, product):
     variant = product.variants.first()
     variant_list = [
         {'id': variant.pk, 'text': '123, Test product (123), $10.00'}]
-
     url = reverse('dashboard:ajax-available-variants')
+
     response = admin_client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-    resp_decoded = json.loads(response.content.decode('utf-8'))
 
+    resp_decoded = json.loads(response.content.decode('utf-8'))
     assert response.status_code == 200
     assert resp_decoded == {'results': variant_list}
 
 
 def test_view_ajax_available_products_list(admin_client, product):
     product_list = [{'id': product.pk, 'text': 'Test product'}]
-
     url = reverse('dashboard:ajax-products')
+
     response = admin_client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-    resp_decoded = json.loads(response.content.decode('utf-8'))
 
+    resp_decoded = json.loads(response.content.decode('utf-8'))
     assert response.status_code == 200
     assert resp_decoded == {'results': product_list}
 
diff --git a/tests/test_registration.py b/tests/test_registration.py
index b33b43dfcd4..917e93437eb 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -112,10 +112,10 @@ def test_signup_view_redirect(client, customer_user):
     url = reverse('account:signup')
     data = {
         'email': 'client@example.com', 'password': 'password',
-        'next': reverse('cart:index')}
+        'next': reverse('checkout:index')}
     response = client.post(url, data)
     redirect_location = get_redirect_location(response)
-    assert redirect_location == reverse('cart:index')
+    assert redirect_location == reverse('checkout:index')
 
 
 def test_signup_view_fail(client, db, customer_user):
diff --git a/tests/test_seo_schema.py b/tests/test_seo_schema.py
index 60f79d7cb94..88fa620c383 100644
--- a/tests/test_seo_schema.py
+++ b/tests/test_seo_schema.py
@@ -16,32 +16,29 @@ def test_get_organization(site_settings):
     assert result['name'] == example_name
 
 
-def test_get_product_data_without_image(order_with_lines_and_stock):
+def test_get_product_data_without_image(order_with_lines):
     """Tested OrderLine Product has no image assigned."""
-    order_line = order_with_lines_and_stock.lines.first()
+    line = order_with_lines.lines.first()
     organization = get_organization()
-    result = get_product_data(order_line, organization)
+    result = get_product_data(line, organization)
     assert 'image' not in result['itemOffered']
 
 
-def test_get_product_data_with_image(
-        order_with_lines_and_stock, product_with_image):
-    order = order_with_lines_and_stock
-    order_line = order.lines.first()
+def test_get_product_data_with_image(order_with_lines, product_with_image):
+    line = order_with_lines.lines.first()
     variant = product_with_image.variants.first()
-    order_line.variant = variant
-    order_line.product_name = variant.display_product()
-    order_line.save()
+    line.variant = variant
+    line.product_name = variant.display_product()
+    line.save()
     organization = get_organization()
-    result = get_product_data(order_line, organization)
+    result = get_product_data(line, organization)
     assert 'image' in result['itemOffered']
     assert result['itemOffered']['name'] == variant.display_product()
 
 
-def test_get_order_confirmation_markup(order_with_lines_and_stock):
-    order = order_with_lines_and_stock
+def test_get_order_confirmation_markup(order_with_lines):
     try:
-        result = get_order_confirmation_markup(order)
+        result = get_order_confirmation_markup(order_with_lines)
     except TypeError:
         pytest.fail('Function output is not JSON serializable')
 
diff --git a/tests/utils.py b/tests/utils.py
index b6d991f0113..a3aec151218 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -30,3 +30,12 @@ def get_graphql_content(response):
 def get_form_errors(response, form_name='form'):
     errors = response.context.get(form_name).errors
     return errors.get('__all__') if errors else []
+
+
+def compare_taxes(taxes_1, taxes_2):
+    assert len(taxes_1) == len(taxes_2)
+
+    for rate_name, tax in taxes_1.items():
+        value_1 = tax['value']
+        value_2 = taxes_2.get(rate_name)['value']
+        assert value_1 == value_2


Code review comments:
diff_hunk: @@ -1,27 +1,619 @@
-from django.utils.translation import pgettext
+"""Cart-related utility functions."""
+from datetime import date, timedelta
+from functools import wraps
+from uuid import UUID
 
-from ..cart.utils import (
-    get_category_variants_and_prices, get_product_variants_and_prices)
+from django.conf import settings
+from django.contrib import messages
+from django.db import transaction
+from django.db.models import Sum
+from django.utils.encoding import smart_text
+from django.utils.translation import get_language, pgettext, pgettext_lazy
+from prices import TaxedMoneyRange
+
+from . import AddressType, logger
+from ..account.forms import get_address_form
+from ..account.models import Address
+from ..account.utils import store_user_address
+from ..core.exceptions import InsufficientStock
+from ..core.utils import to_local_currency
+from ..core.utils.taxes import ZERO_MONEY, get_taxes_for_country
 from ..discount import VoucherType
-from ..discount.models import NotApplicable
+from ..discount.models import NotApplicable, Voucher
 from ..discount.utils import (
     get_product_or_category_voucher_discount, get_shipping_voucher_discount,
-    get_value_voucher_discount)
+    get_value_voucher_discount, increase_voucher_usage)
+from ..order.models import Order
+from ..shipping.models import ShippingMethodCountry
+from .forms import (
+    AddressChoiceForm, AnonymousUserBillingForm, AnonymousUserShippingForm,
+    BillingAddressChoiceForm)
+from .models import Cart
+
+COOKIE_NAME = 'cart'
+
+
+def set_cart_cookie(simple_cart, response):
+    """Update response with a cart token cookie."""
+    # FIXME: document why session is not used
+    max_age = int(timedelta(days=30).total_seconds())
+    response.set_signed_cookie(COOKIE_NAME, simple_cart.token, max_age=max_age)
+
+
+def contains_unavailable_variants(cart):
+    """Return `True` if cart contains any unfulfillable lines."""
+    try:
+        for line in cart:
+            line.variant.check_quantity(line.quantity)
+    except InsufficientStock:
+        return True
+    return False
+
+
+def token_is_valid(token):
+    """Validate a cart token."""
+    if token is None:
+        return False
+    if isinstance(token, UUID):
+        return True
+    try:
+        UUID(token)
+    except ValueError:
+        return False
+    return True
+
+
+def remove_unavailable_variants(cart):
+    """Remove any unavailable items from cart."""
+    for line in cart:
+        try:
+            add_variant_to_cart(
+                cart, line.variant, line.quantity, replace=True)
+        except InsufficientStock as e:
+            quantity = e.item.quantity_available
+            add_variant_to_cart(cart, line.variant, quantity, replace=True)
+
+
+def get_product_variants_and_prices(cart, product):
+    """Get variants and unit prices from cart lines matching the product."""
+    lines = (line for line in cart if line.variant.product_id == product.id)
+    for line in lines:
+        for dummy_i in range(line.quantity):
+            yield line.variant, line.variant.get_price()
+
+
+def get_category_variants_and_prices(cart, root_category):
+    """Get variants and unit prices from cart lines matching the category.
+
+    Product is assumed to be in the category if it belongs to any of its
+    descendant subcategories.
+    """
+    matching_products = {
+        line.variant.product for line in cart
+        if line.variant.product.category.is_descendant_of(
+            root_category, include_self=True)}
+    for product in matching_products:
+        for line in get_product_variants_and_prices(cart, product):
+            yield line
+
+
+def check_product_availability_and_warn(request, cart):
+    """Warn if cart contains any lines that cannot be fulfilled."""
+    if contains_unavailable_variants(cart):
+        msg = pgettext_lazy(
+            'Cart warning message',
+            'Sorry. We don\'t have that many items in stock. '
+            'Quantity was set to maximum available for now.')
+        messages.warning(request, msg)
+        remove_unavailable_variants(cart)
+
+
+def find_and_assign_anonymous_cart(queryset=Cart.objects.all()):
+    """Assign cart from cookie to request user."""
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            response = view(request, *args, **kwargs)
+            token = request.get_signed_cookie(COOKIE_NAME, default=None)
+            if not token_is_valid(token):
+                return response
+            cart = get_anonymous_cart_from_token(
+                token=token, cart_queryset=queryset)
+            if cart is None:
+                return response
+            if request.user.is_authenticated:
+                with transaction.atomic():
+                    change_cart_user(cart, request.user)
+                    carts_to_close = Cart.objects.filter(user=request.user)
+                    carts_to_close = carts_to_close.exclude(token=token)
+                    carts_to_close.delete()
+                response.delete_cookie(COOKIE_NAME)
+            return response
+
+        return func
+    return get_cart
+
+
+def get_or_create_anonymous_cart_from_token(
+        token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token or create a new one."""
+    return cart_queryset.filter(token=token, user=None).get_or_create(
+        defaults={'user': None})[0]
+
+
+def get_or_create_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user or create a new one."""
+    defaults = {
+        'shipping_address': user.default_shipping_address,
+        'billing_address': user.default_billing_address}
+    return cart_queryset.get_or_create(user=user, defaults=defaults)[0]
+
+
+def get_anonymous_cart_from_token(token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token if any."""
+    return cart_queryset.filter(token=token, user=None).first()
+
+
+def get_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user if any."""
+    return cart_queryset.filter(user=user).first()
+
+
+def get_or_create_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or create a new one based on cookie."""
+    if request.user.is_authenticated:
+        return get_or_create_user_cart(request.user, cart_queryset)
+    token = request.get_signed_cookie(COOKIE_NAME, default=None)
+    return get_or_create_anonymous_cart_from_token(token, cart_queryset)
+
+
+def get_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or return a new instance based on cookie."""
+    if request.user.is_authenticated:
+        cart = get_user_cart(request.user, cart_queryset)
+        user = request.user
+    else:
+        token = request.get_signed_cookie(COOKIE_NAME, default=None)
+        cart = get_anonymous_cart_from_token(token, cart_queryset)
+        user = None
+    if cart is not None:
+        return cart
+    if user:
+        return Cart(user=user)
+    return Cart()
+
+
+def get_or_create_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to always receive a saved cart instance.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, one will be created and a cookie will be set
+    for users who are not logged in.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_or_create_cart_from_request(request, cart_queryset)
+            response = view(request, cart, *args, **kwargs)
+            if not request.user.is_authenticated:
+                set_cart_cookie(cart, response)
+            return response
+        return func
+    return get_cart
+
+
+def get_or_empty_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to receive a cart if one exists.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, an unsaved `Cart` instance will be used.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_cart_from_request(request, cart_queryset)
+            return view(request, cart, *args, **kwargs)
+        return func
+    return get_cart
+
+
+def get_cart_data(cart, shipping_range, currency, discounts, taxes):
+    """Return a JSON-serializable representation of the cart."""
+    cart_total = None
+    local_cart_total = None
+    shipping_required = False
+    total_with_shipping = None
+    local_total_with_shipping = None
+    if cart:
+        cart_total = cart.get_subtotal(discounts, taxes)
+        local_cart_total = to_local_currency(cart_total, currency)
+        shipping_required = cart.is_shipping_required()
+        total_with_shipping = TaxedMoneyRange(
+            start=cart_total, stop=cart_total)
+        if shipping_required and shipping_range:
+            total_with_shipping = shipping_range + cart_total
+        local_total_with_shipping = to_local_currency(
+            total_with_shipping, currency)
+
+    return {
+        'cart_total': cart_total,
+        'local_cart_total': local_cart_total,
+        'shipping_required': shipping_required,
+        'total_with_shipping': total_with_shipping,
+        'local_total_with_shipping': local_total_with_shipping}
+
+
+def find_open_cart_for_user(user):
+    """Find an open cart for the given user."""
+    carts = user.carts.all()
+    if len(carts) > 1:
+        logger.warning('%s has more than one open basket', user)
+        for cart in carts[1:]:
+            cart.delete()
+    return carts.first()
+
+
+def change_cart_user(cart, user):
+    """Assign cart to a user.
+
+    If the user already has an open cart assigned, cancel it.
+    """
+    open_cart = find_open_cart_for_user(user)
+    if open_cart is not None:
+        open_cart.delete()
+    cart.user = user
+    cart.shipping_address = user.default_shipping_address
+    cart.billing_address = user.default_billing_address
+    cart.save(update_fields=['user', 'shipping_address', 'billing_address'])
+
+
+def update_cart_quantity(cart):
+    """Update the total quantity in cart."""
+    total_lines = cart.lines.aggregate(
+        total_quantity=Sum('quantity'))['total_quantity']
+    if not total_lines:
+        total_lines = 0
+    cart.quantity = total_lines
+    cart.save(update_fields=['quantity'])
+
+
+def add_variant_to_cart(
+        cart, variant, quantity=1, replace=False, check_quantity=True):
+    """Add a product variant to cart.
+
+    The `data` parameter may be used to differentiate between items with
+    different customization options.
+
+    If `replace` is truthy then any previous quantity is discarded instead
+    of added to.
+    """
+    line, _ = cart.lines.get_or_create(
+        variant=variant, defaults={'quantity': 0, 'data': {}})
+    new_quantity = quantity if replace else (quantity + line.quantity)
+
+    if new_quantity < 0:
+        raise ValueError('%r is not a valid quantity (results in %r)' % (
+            quantity, new_quantity))
+
+    if new_quantity == 0:
+        line.delete()
+    else:
+        if check_quantity:
+            variant.check_quantity(new_quantity)
+
+        line.quantity = new_quantity
+        line.save(update_fields=['quantity'])
+
+    update_cart_quantity(cart)
+
+
+def get_shipping_address_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on shipping address in cart."""
+    shipping_address = (
+        cart.shipping_address or cart.user.default_shipping_address)
+
+    if shipping_address and shipping_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses,
+            initial={'address': shipping_address.id})
+    elif shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            instance=shipping_address)
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+
+    return address_form, addresses_form, preview
+
+
+def update_shipping_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_shipping_address_forms(cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def update_shipping_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_address_form(
+        data, country_code=country.code,
+        autocomplete_type='shipping',
+        instance=cart.shipping_address,
+        initial={'country': country})
+    user_form = AnonymousUserShippingForm(
+        data if not preview else None, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid():
+        user_form.save()
+        address = address_form.save()
+        change_shipping_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
 
+def get_billing_forms_with_shipping(cart, data, user_addresses, country):
+    """Get billing form based on a the current billing and shipping data."""
+    shipping_address = cart.shipping_address
+    billing_address = cart.billing_address or Address(country=country)
 
-def _get_value_voucher_discount_for_checkout(voucher, checkout):
+    if not billing_address.id or billing_address == shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': shipping_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.SHIPPING_ADDRESS})
+    elif billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': billing_address.id})
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.NEW_ADDRESS})
+
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart_with_shipping(
+        cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = get_billing_forms_with_shipping(
+        cart, data, user_addresses, country)
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        address = None
+        address_id = addresses_form.cleaned_data['address']
+
+        if address_id == BillingAddressChoiceForm.SHIPPING_ADDRESS:
+            if cart.user and cart.shipping_address in user_addresses:
+                address = cart.shipping_address
+            else:
+                address = cart.shipping_address.get_copy()
+        elif address_id != BillingAddressChoiceForm.NEW_ADDRESS:
+            address = user_addresses.get(id=address_id)
+        elif address_form.is_valid():
+            address = address_form.save()
+
+        if address:
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def get_anonymous_summary_without_shipping_forms(cart, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing', instance=billing_address)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            autocomplete_type='billing', initial={'country': country})
+
+    return address_form, preview
+
+
+def update_billing_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_anonymous_summary_without_shipping_forms(
+        cart, data, country)
+    user_form = AnonymousUserBillingForm(data, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid() and not preview:
+        user_form.save()
+        address = address_form.save()
+        change_billing_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
+
+def get_summary_without_shipping_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address and billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country})
+        initial_address = billing_address.id
+    elif billing_address:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        initial_address = AddressChoiceForm.NEW_ADDRESS
+    else:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=country.code,
+            initial={'country': country})
+        if cart.user and cart.user.default_billing_address:
+            initial_address = cart.user.default_billing_address.id
+        else:
+            initial_address = AddressChoiceForm.NEW_ADDRESS
+
+    addresses_form = AddressChoiceForm(
+        data, addresses=user_addresses, initial={'address': initial_address})
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_summary_without_shipping_forms(
+            cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid():
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def change_billing_address_in_cart(cart, address):
+    """Save billing address in cart if changed.
+
+    Remove previously saved address if not connected to any user.
+    """
+    has_address_changed = (
+        not address and cart.billing_address or
+        address and not cart.billing_address or
+        address and cart.billing_address and address != cart.billing_address)
+    if has_address_changed:
+        remove_old_address = (
+            cart.billing_address and (not cart.user or (
+                cart.user and
+                cart.billing_address not in cart.user.addresses.all())))
+        if remove_old_address:
+            cart.billing_address.delete()
+        cart.billing_address = address
+        cart.save(update_fields=['billing_address'])
+
+
+def change_shipping_address_in_cart(cart, address):
+    """Save shipping address in cart if changed.
+
+    Remove previously saved address if not connected to any user.
+    """
+    has_address_changed = (
+        not address and cart.shipping_address or
+        address and not cart.shipping_address or
+        address and cart.shipping_address and address != cart.shipping_address)
+    if has_address_changed:
+        remove_old_address = (
+            cart.shipping_address and (not cart.user or (
path: saleor/checkout/utils.py
text: @user1:
Not a fan of such nesting, can we introduce some variables? 

@author:
I will extract a separate function checking if an address has changed and should be removed.

diff_hunk: @@ -1,27 +1,619 @@
-from django.utils.translation import pgettext
+"""Cart-related utility functions."""
+from datetime import date, timedelta
+from functools import wraps
+from uuid import UUID
 
-from ..cart.utils import (
-    get_category_variants_and_prices, get_product_variants_and_prices)
+from django.conf import settings
+from django.contrib import messages
+from django.db import transaction
+from django.db.models import Sum
+from django.utils.encoding import smart_text
+from django.utils.translation import get_language, pgettext, pgettext_lazy
+from prices import TaxedMoneyRange
+
+from . import AddressType, logger
+from ..account.forms import get_address_form
+from ..account.models import Address
+from ..account.utils import store_user_address
+from ..core.exceptions import InsufficientStock
+from ..core.utils import to_local_currency
+from ..core.utils.taxes import ZERO_MONEY, get_taxes_for_country
 from ..discount import VoucherType
-from ..discount.models import NotApplicable
+from ..discount.models import NotApplicable, Voucher
 from ..discount.utils import (
     get_product_or_category_voucher_discount, get_shipping_voucher_discount,
-    get_value_voucher_discount)
+    get_value_voucher_discount, increase_voucher_usage)
+from ..order.models import Order
+from ..shipping.models import ShippingMethodCountry
+from .forms import (
+    AddressChoiceForm, AnonymousUserBillingForm, AnonymousUserShippingForm,
+    BillingAddressChoiceForm)
+from .models import Cart
+
+COOKIE_NAME = 'cart'
+
+
+def set_cart_cookie(simple_cart, response):
+    """Update response with a cart token cookie."""
+    # FIXME: document why session is not used
+    max_age = int(timedelta(days=30).total_seconds())
+    response.set_signed_cookie(COOKIE_NAME, simple_cart.token, max_age=max_age)
+
+
+def contains_unavailable_variants(cart):
+    """Return `True` if cart contains any unfulfillable lines."""
+    try:
+        for line in cart:
+            line.variant.check_quantity(line.quantity)
+    except InsufficientStock:
+        return True
+    return False
+
+
+def token_is_valid(token):
+    """Validate a cart token."""
+    if token is None:
+        return False
+    if isinstance(token, UUID):
+        return True
+    try:
+        UUID(token)
+    except ValueError:
+        return False
+    return True
+
+
+def remove_unavailable_variants(cart):
+    """Remove any unavailable items from cart."""
+    for line in cart:
+        try:
+            add_variant_to_cart(
+                cart, line.variant, line.quantity, replace=True)
+        except InsufficientStock as e:
+            quantity = e.item.quantity_available
+            add_variant_to_cart(cart, line.variant, quantity, replace=True)
+
+
+def get_product_variants_and_prices(cart, product):
+    """Get variants and unit prices from cart lines matching the product."""
+    lines = (line for line in cart if line.variant.product_id == product.id)
+    for line in lines:
+        for dummy_i in range(line.quantity):
+            yield line.variant, line.variant.get_price()
+
+
+def get_category_variants_and_prices(cart, root_category):
+    """Get variants and unit prices from cart lines matching the category.
+
+    Product is assumed to be in the category if it belongs to any of its
+    descendant subcategories.
+    """
+    matching_products = {
+        line.variant.product for line in cart
+        if line.variant.product.category.is_descendant_of(
+            root_category, include_self=True)}
+    for product in matching_products:
+        for line in get_product_variants_and_prices(cart, product):
+            yield line
+
+
+def check_product_availability_and_warn(request, cart):
+    """Warn if cart contains any lines that cannot be fulfilled."""
+    if contains_unavailable_variants(cart):
+        msg = pgettext_lazy(
+            'Cart warning message',
+            'Sorry. We don\'t have that many items in stock. '
+            'Quantity was set to maximum available for now.')
+        messages.warning(request, msg)
+        remove_unavailable_variants(cart)
+
+
+def find_and_assign_anonymous_cart(queryset=Cart.objects.all()):
+    """Assign cart from cookie to request user."""
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            response = view(request, *args, **kwargs)
+            token = request.get_signed_cookie(COOKIE_NAME, default=None)
+            if not token_is_valid(token):
+                return response
+            cart = get_anonymous_cart_from_token(
+                token=token, cart_queryset=queryset)
+            if cart is None:
+                return response
+            if request.user.is_authenticated:
+                with transaction.atomic():
+                    change_cart_user(cart, request.user)
+                    carts_to_close = Cart.objects.filter(user=request.user)
+                    carts_to_close = carts_to_close.exclude(token=token)
+                    carts_to_close.delete()
+                response.delete_cookie(COOKIE_NAME)
+            return response
+
+        return func
+    return get_cart
+
+
+def get_or_create_anonymous_cart_from_token(
+        token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token or create a new one."""
+    return cart_queryset.filter(token=token, user=None).get_or_create(
+        defaults={'user': None})[0]
+
+
+def get_or_create_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user or create a new one."""
+    defaults = {
+        'shipping_address': user.default_shipping_address,
+        'billing_address': user.default_billing_address}
+    return cart_queryset.get_or_create(user=user, defaults=defaults)[0]
+
+
+def get_anonymous_cart_from_token(token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token if any."""
+    return cart_queryset.filter(token=token, user=None).first()
+
+
+def get_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user if any."""
+    return cart_queryset.filter(user=user).first()
+
+
+def get_or_create_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or create a new one based on cookie."""
+    if request.user.is_authenticated:
+        return get_or_create_user_cart(request.user, cart_queryset)
+    token = request.get_signed_cookie(COOKIE_NAME, default=None)
+    return get_or_create_anonymous_cart_from_token(token, cart_queryset)
+
+
+def get_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or return a new instance based on cookie."""
+    if request.user.is_authenticated:
+        cart = get_user_cart(request.user, cart_queryset)
+        user = request.user
+    else:
+        token = request.get_signed_cookie(COOKIE_NAME, default=None)
+        cart = get_anonymous_cart_from_token(token, cart_queryset)
+        user = None
+    if cart is not None:
+        return cart
+    if user:
+        return Cart(user=user)
+    return Cart()
+
+
+def get_or_create_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to always receive a saved cart instance.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, one will be created and a cookie will be set
+    for users who are not logged in.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_or_create_cart_from_request(request, cart_queryset)
+            response = view(request, cart, *args, **kwargs)
+            if not request.user.is_authenticated:
+                set_cart_cookie(cart, response)
+            return response
+        return func
+    return get_cart
+
+
+def get_or_empty_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to receive a cart if one exists.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, an unsaved `Cart` instance will be used.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_cart_from_request(request, cart_queryset)
+            return view(request, cart, *args, **kwargs)
+        return func
+    return get_cart
+
+
+def get_cart_data(cart, shipping_range, currency, discounts, taxes):
+    """Return a JSON-serializable representation of the cart."""
+    cart_total = None
+    local_cart_total = None
+    shipping_required = False
+    total_with_shipping = None
+    local_total_with_shipping = None
+    if cart:
+        cart_total = cart.get_subtotal(discounts, taxes)
+        local_cart_total = to_local_currency(cart_total, currency)
+        shipping_required = cart.is_shipping_required()
+        total_with_shipping = TaxedMoneyRange(
+            start=cart_total, stop=cart_total)
+        if shipping_required and shipping_range:
+            total_with_shipping = shipping_range + cart_total
+        local_total_with_shipping = to_local_currency(
+            total_with_shipping, currency)
+
+    return {
+        'cart_total': cart_total,
+        'local_cart_total': local_cart_total,
+        'shipping_required': shipping_required,
+        'total_with_shipping': total_with_shipping,
+        'local_total_with_shipping': local_total_with_shipping}
+
+
+def find_open_cart_for_user(user):
+    """Find an open cart for the given user."""
+    carts = user.carts.all()
+    if len(carts) > 1:
+        logger.warning('%s has more than one open basket', user)
+        for cart in carts[1:]:
+            cart.delete()
+    return carts.first()
+
+
+def change_cart_user(cart, user):
+    """Assign cart to a user.
+
+    If the user already has an open cart assigned, cancel it.
+    """
+    open_cart = find_open_cart_for_user(user)
+    if open_cart is not None:
+        open_cart.delete()
+    cart.user = user
+    cart.shipping_address = user.default_shipping_address
+    cart.billing_address = user.default_billing_address
+    cart.save(update_fields=['user', 'shipping_address', 'billing_address'])
+
+
+def update_cart_quantity(cart):
+    """Update the total quantity in cart."""
+    total_lines = cart.lines.aggregate(
+        total_quantity=Sum('quantity'))['total_quantity']
+    if not total_lines:
+        total_lines = 0
+    cart.quantity = total_lines
+    cart.save(update_fields=['quantity'])
+
+
+def add_variant_to_cart(
+        cart, variant, quantity=1, replace=False, check_quantity=True):
+    """Add a product variant to cart.
+
+    The `data` parameter may be used to differentiate between items with
+    different customization options.
+
+    If `replace` is truthy then any previous quantity is discarded instead
+    of added to.
+    """
+    line, _ = cart.lines.get_or_create(
+        variant=variant, defaults={'quantity': 0, 'data': {}})
+    new_quantity = quantity if replace else (quantity + line.quantity)
+
+    if new_quantity < 0:
+        raise ValueError('%r is not a valid quantity (results in %r)' % (
+            quantity, new_quantity))
+
+    if new_quantity == 0:
+        line.delete()
+    else:
+        if check_quantity:
+            variant.check_quantity(new_quantity)
+
+        line.quantity = new_quantity
+        line.save(update_fields=['quantity'])
+
+    update_cart_quantity(cart)
+
+
+def get_shipping_address_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on shipping address in cart."""
+    shipping_address = (
+        cart.shipping_address or cart.user.default_shipping_address)
+
+    if shipping_address and shipping_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses,
+            initial={'address': shipping_address.id})
+    elif shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            instance=shipping_address)
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            initial={'country': country})
+        addresses_form = AddressChoiceForm(
+            data, addresses=user_addresses)
+
+    return address_form, addresses_form, preview
+
+
+def update_shipping_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_shipping_address_forms(cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_shipping_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def update_shipping_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_address_form(
+        data, country_code=country.code,
+        autocomplete_type='shipping',
+        instance=cart.shipping_address,
+        initial={'country': country})
+    user_form = AnonymousUserShippingForm(
+        data if not preview else None, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid():
+        user_form.save()
+        address = address_form.save()
+        change_shipping_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
 
+def get_billing_forms_with_shipping(cart, data, user_addresses, country):
+    """Get billing form based on a the current billing and shipping data."""
+    shipping_address = cart.shipping_address
+    billing_address = cart.billing_address or Address(country=country)
 
-def _get_value_voucher_discount_for_checkout(voucher, checkout):
+    if not billing_address.id or billing_address == shipping_address:
+        address_form, preview = get_address_form(
+            data, country_code=shipping_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': shipping_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.SHIPPING_ADDRESS})
+    elif billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country})
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': billing_address.id})
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing',
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        addresses_form = BillingAddressChoiceForm(
+            data, addresses=user_addresses, initial={
+                'address': BillingAddressChoiceForm.NEW_ADDRESS})
+
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart_with_shipping(
+        cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = get_billing_forms_with_shipping(
+        cart, data, user_addresses, country)
+
+    updated = False
+
+    if addresses_form.is_valid() and not preview:
+        address = None
+        address_id = addresses_form.cleaned_data['address']
+
+        if address_id == BillingAddressChoiceForm.SHIPPING_ADDRESS:
+            if cart.user and cart.shipping_address in user_addresses:
+                address = cart.shipping_address
+            else:
+                address = cart.shipping_address.get_copy()
+        elif address_id != BillingAddressChoiceForm.NEW_ADDRESS:
+            address = user_addresses.get(id=address_id)
+        elif address_form.is_valid():
+            address = address_form.save()
+
+        if address:
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def get_anonymous_summary_without_shipping_forms(cart, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address:
+        address_form, preview = get_address_form(
+            data, country_code=billing_address.country.code,
+            autocomplete_type='billing', instance=billing_address)
+    else:
+        address_form, preview = get_address_form(
+            data, country_code=country.code,
+            autocomplete_type='billing', initial={'country': country})
+
+    return address_form, preview
+
+
+def update_billing_address_in_anonymous_cart(cart, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, preview = get_anonymous_summary_without_shipping_forms(
+        cart, data, country)
+    user_form = AnonymousUserBillingForm(data, instance=cart)
+
+    updated = False
+
+    if user_form.is_valid() and address_form.is_valid() and not preview:
+        user_form.save()
+        address = address_form.save()
+        change_billing_address_in_cart(cart, address)
+        updated = True
+
+    return user_form, address_form, updated
+
+
+def get_summary_without_shipping_forms(cart, user_addresses, data, country):
+    """Forms initialized with data depending on addresses in cart."""
+    billing_address = cart.billing_address
+
+    if billing_address and billing_address in user_addresses:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country})
+        initial_address = billing_address.id
+    elif billing_address:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=billing_address.country.code,
+            initial={'country': billing_address.country},
+            instance=billing_address)
+        initial_address = AddressChoiceForm.NEW_ADDRESS
+    else:
+        address_form, preview = get_address_form(
+            data,
+            autocomplete_type='billing',
+            country_code=country.code,
+            initial={'country': country})
+        if cart.user and cart.user.default_billing_address:
+            initial_address = cart.user.default_billing_address.id
+        else:
+            initial_address = AddressChoiceForm.NEW_ADDRESS
+
+    addresses_form = AddressChoiceForm(
+        data, addresses=user_addresses, initial={'address': initial_address})
+    return address_form, addresses_form, preview
+
+
+def update_billing_address_in_cart(cart, user_addresses, data, country):
+    """Return shipping address choice forms and if an address was updated."""
+    address_form, addresses_form, preview = (
+        get_summary_without_shipping_forms(
+            cart, user_addresses, data, country))
+
+    updated = False
+
+    if addresses_form.is_valid():
+        use_existing_address = (
+            addresses_form.cleaned_data['address'] !=
+            AddressChoiceForm.NEW_ADDRESS)
+
+        if use_existing_address:
+            address_id = addresses_form.cleaned_data['address']
+            address = Address.objects.get(id=address_id)
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+        elif address_form.is_valid():
+            address = address_form.save()
+            change_billing_address_in_cart(cart, address)
+            updated = True
+
+    return addresses_form, address_form, updated
+
+
+def change_billing_address_in_cart(cart, address):
+    """Save billing address in cart if changed.
+
+    Remove previously saved address if not connected to any user.
+    """
+    has_address_changed = (
path: saleor/checkout/utils.py
text: @user1:
can we introduce some kind of variables, or use `all`?  We are checking presence of 7 variables here 

@author:
As above, I moved it to a new function.

diff_hunk: @@ -1,27 +1,619 @@
-from django.utils.translation import pgettext
+"""Cart-related utility functions."""
+from datetime import date, timedelta
+from functools import wraps
+from uuid import UUID
 
-from ..cart.utils import (
-    get_category_variants_and_prices, get_product_variants_and_prices)
+from django.conf import settings
+from django.contrib import messages
+from django.db import transaction
+from django.db.models import Sum
+from django.utils.encoding import smart_text
+from django.utils.translation import get_language, pgettext, pgettext_lazy
+from prices import TaxedMoneyRange
+
+from . import AddressType, logger
+from ..account.forms import get_address_form
+from ..account.models import Address
+from ..account.utils import store_user_address
+from ..core.exceptions import InsufficientStock
+from ..core.utils import to_local_currency
+from ..core.utils.taxes import ZERO_MONEY, get_taxes_for_country
 from ..discount import VoucherType
-from ..discount.models import NotApplicable
+from ..discount.models import NotApplicable, Voucher
 from ..discount.utils import (
     get_product_or_category_voucher_discount, get_shipping_voucher_discount,
-    get_value_voucher_discount)
+    get_value_voucher_discount, increase_voucher_usage)
+from ..order.models import Order
+from ..shipping.models import ShippingMethodCountry
+from .forms import (
+    AddressChoiceForm, AnonymousUserBillingForm, AnonymousUserShippingForm,
+    BillingAddressChoiceForm)
+from .models import Cart
+
+COOKIE_NAME = 'cart'
+
+
+def set_cart_cookie(simple_cart, response):
+    """Update response with a cart token cookie."""
+    # FIXME: document why session is not used
+    max_age = int(timedelta(days=30).total_seconds())
+    response.set_signed_cookie(COOKIE_NAME, simple_cart.token, max_age=max_age)
+
+
+def contains_unavailable_variants(cart):
+    """Return `True` if cart contains any unfulfillable lines."""
+    try:
+        for line in cart:
+            line.variant.check_quantity(line.quantity)
+    except InsufficientStock:
+        return True
+    return False
+
+
+def token_is_valid(token):
+    """Validate a cart token."""
+    if token is None:
+        return False
+    if isinstance(token, UUID):
+        return True
+    try:
+        UUID(token)
+    except ValueError:
+        return False
+    return True
+
+
+def remove_unavailable_variants(cart):
+    """Remove any unavailable items from cart."""
+    for line in cart:
+        try:
+            add_variant_to_cart(
+                cart, line.variant, line.quantity, replace=True)
+        except InsufficientStock as e:
+            quantity = e.item.quantity_available
+            add_variant_to_cart(cart, line.variant, quantity, replace=True)
+
+
+def get_product_variants_and_prices(cart, product):
+    """Get variants and unit prices from cart lines matching the product."""
+    lines = (line for line in cart if line.variant.product_id == product.id)
+    for line in lines:
+        for dummy_i in range(line.quantity):
+            yield line.variant, line.variant.get_price()
+
+
+def get_category_variants_and_prices(cart, root_category):
+    """Get variants and unit prices from cart lines matching the category.
+
+    Product is assumed to be in the category if it belongs to any of its
+    descendant subcategories.
+    """
+    matching_products = {
+        line.variant.product for line in cart
+        if line.variant.product.category.is_descendant_of(
+            root_category, include_self=True)}
+    for product in matching_products:
+        for line in get_product_variants_and_prices(cart, product):
+            yield line
+
+
+def check_product_availability_and_warn(request, cart):
+    """Warn if cart contains any lines that cannot be fulfilled."""
+    if contains_unavailable_variants(cart):
+        msg = pgettext_lazy(
+            'Cart warning message',
+            'Sorry. We don\'t have that many items in stock. '
+            'Quantity was set to maximum available for now.')
+        messages.warning(request, msg)
+        remove_unavailable_variants(cart)
+
+
+def find_and_assign_anonymous_cart(queryset=Cart.objects.all()):
+    """Assign cart from cookie to request user."""
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            response = view(request, *args, **kwargs)
+            token = request.get_signed_cookie(COOKIE_NAME, default=None)
+            if not token_is_valid(token):
+                return response
+            cart = get_anonymous_cart_from_token(
+                token=token, cart_queryset=queryset)
+            if cart is None:
+                return response
+            if request.user.is_authenticated:
+                with transaction.atomic():
+                    change_cart_user(cart, request.user)
+                    carts_to_close = Cart.objects.filter(user=request.user)
+                    carts_to_close = carts_to_close.exclude(token=token)
+                    carts_to_close.delete()
+                response.delete_cookie(COOKIE_NAME)
+            return response
+
+        return func
+    return get_cart
+
+
+def get_or_create_anonymous_cart_from_token(
+        token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token or create a new one."""
+    return cart_queryset.filter(token=token, user=None).get_or_create(
+        defaults={'user': None})[0]
+
+
+def get_or_create_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user or create a new one."""
+    defaults = {
+        'shipping_address': user.default_shipping_address,
+        'billing_address': user.default_billing_address}
+    return cart_queryset.get_or_create(user=user, defaults=defaults)[0]
+
+
+def get_anonymous_cart_from_token(token, cart_queryset=Cart.objects.all()):
+    """Return an open unassigned cart with given token if any."""
+    return cart_queryset.filter(token=token, user=None).first()
+
+
+def get_user_cart(user, cart_queryset=Cart.objects.all()):
+    """Return an open cart for given user if any."""
+    return cart_queryset.filter(user=user).first()
+
+
+def get_or_create_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or create a new one based on cookie."""
+    if request.user.is_authenticated:
+        return get_or_create_user_cart(request.user, cart_queryset)
+    token = request.get_signed_cookie(COOKIE_NAME, default=None)
+    return get_or_create_anonymous_cart_from_token(token, cart_queryset)
+
+
+def get_cart_from_request(request, cart_queryset=Cart.objects.all()):
+    """Fetch cart from database or return a new instance based on cookie."""
+    if request.user.is_authenticated:
+        cart = get_user_cart(request.user, cart_queryset)
+        user = request.user
+    else:
+        token = request.get_signed_cookie(COOKIE_NAME, default=None)
+        cart = get_anonymous_cart_from_token(token, cart_queryset)
+        user = None
+    if cart is not None:
+        return cart
+    if user:
+        return Cart(user=user)
+    return Cart()
+
+
+def get_or_create_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to always receive a saved cart instance.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, one will be created and a cookie will be set
+    for users who are not logged in.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_or_create_cart_from_request(request, cart_queryset)
+            response = view(request, cart, *args, **kwargs)
+            if not request.user.is_authenticated:
+                set_cart_cookie(cart, response)
+            return response
+        return func
+    return get_cart
+
+
+def get_or_empty_db_cart(cart_queryset=Cart.objects.all()):
+    """Decorate view to receive a cart if one exists.
+
+    Changes the view signature from `func(request, ...)` to
+    `func(request, cart, ...)`.
+
+    If no matching cart is found, an unsaved `Cart` instance will be used.
+    """
+    # FIXME: behave like middleware and assign cart to request instead
+    def get_cart(view):
+        @wraps(view)
+        def func(request, *args, **kwargs):
+            cart = get_cart_from_request(request, cart_queryset)
+            return view(request, cart, *args, **kwargs)
+        return func
+    return get_cart
+
+
+def get_cart_data(cart, shipping_range, currency, discounts, taxes):
+    """Return a JSON-serializable representation of the cart."""
+    cart_total = None
+    local_cart_total = None
+    shipping_required = False
+    total_with_shipping = None
+    local_total_with_shipping = None
+    if cart:
+        cart_total = cart.get_subtotal(discounts, taxes)
+        local_cart_total = to_local_currency(cart_total, currency)
+        shipping_required = cart.is_shipping_required()
+        total_with_shipping = TaxedMoneyRange(
+            start=cart_total, stop=cart_total)
+        if shipping_required and shipping_range:
+            total_with_shipping = shipping_range + cart_total
+        local_total_with_shipping = to_local_currency(
+            total_with_shipping, currency)
+
+    return {
+        'cart_total': cart_total,
+        'local_cart_total': local_cart_total,
+        'shipping_required': shipping_required,
+        'total_with_shipping': total_with_shipping,
+        'local_total_with_shipping': local_total_with_shipping}
+
+
+def find_open_cart_for_user(user):
+    """Find an open cart for the given user."""
+    carts = user.carts.all()
+    if len(carts) > 1:
+        logger.warning('%s has more than one open basket', user)
+        for cart in carts[1:]:
path: saleor/checkout/utils.py
text: @user1:
Just checking, the first cart is the newest? Or shall we order above queryset by pk/created/last modified?

@author:
First one is the newest - we track it by Cart's `last_change` field.

diff_hunk: @@ -57,16 +649,162 @@ def _get_product_or_category_voucher_discount_for_checkout(voucher, checkout):
     return get_product_or_category_voucher_discount(voucher, prices)
 
 
-def get_voucher_discount_for_checkout(voucher, checkout):
+def get_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value depending on voucher and discount types.
 
     Raise NotApplicable if voucher of given type cannot be applied.
     """
     if voucher.type == VoucherType.VALUE:
-        return _get_value_voucher_discount_for_checkout(voucher, checkout)
+        return _get_value_voucher_discount_for_cart(voucher, cart)
     if voucher.type == VoucherType.SHIPPING:
-        return _get_shipping_voucher_discount_for_checkout(voucher, checkout)
+        return _get_shipping_voucher_discount_for_cart(voucher, cart)
     if voucher.type in (VoucherType.PRODUCT, VoucherType.CATEGORY):
-        return _get_product_or_category_voucher_discount_for_checkout(
-            voucher, checkout)
+        return _get_product_or_category_voucher_discount_for_cart(
+            voucher, cart)
     raise NotImplementedError('Unknown discount type')
+
+
+def get_voucher_for_cart(cart, vouchers=None):
+    """Return voucher with voucher code saved in cart if active or None."""
+    if cart.voucher_code is not None:
+        if vouchers is None:
+            vouchers = Voucher.objects.active(date=date.today())
+        try:
+            return vouchers.get(code=cart.voucher_code)
+        except Voucher.DoesNotExist:
+            return None
+    return None
+
+
+def recalculate_cart_discount(cart):
+    """Recalculate `cart.discount` based on the voucher.
+
+    Will clear both voucher and discount if the discount is no longer
+    applicable.
+    """
+    voucher = get_voucher_for_cart(cart)
+    if voucher is not None:
+        try:
+            cart.discount_amount = get_voucher_discount_for_cart(voucher, cart)
+            cart.discount_name = voucher.name
+            cart.save(update_fields=['discount_amount', 'discount_name'])
+        except NotApplicable:
+            remove_voucher_from_cart(cart)
+    else:
+        remove_voucher_from_cart(cart)
+
+
+def remove_voucher_from_cart(cart):
+    """Remove voucher data from cart."""
+    cart.voucher_code = None
+    cart.discount_name = None
+    cart.discount_amount = ZERO_MONEY
+    cart.save(update_fields=[
+        'voucher_code', 'discount_name', 'discount_amount'])
+
+
+def get_taxes_for_cart(cart, default_taxes):
+    """Return taxes (if handled) due to shipping address or default one."""
+    if not settings.VATLAYER_ACCESS_KEY:
+        return None
+
+    if cart.shipping_address:
+        return get_taxes_for_country(cart.shipping_address.country)
+
+    return default_taxes
+
+
+def check_shipping_method(cart):
+    """Check if shipping method is valid for address and remove (if not)."""
+    country_code = cart.shipping_address.country.code
+    valid_methods = ShippingMethodCountry.objects.select_related(
+        'shipping_method').unique_for_country_code(country_code)
+    if cart.shipping_method not in valid_methods:
+        cart.shipping_method = None
+        cart.save()
+        return False
+    return True
+
+
+@transaction.atomic
+def create_order(cart, tracking_code, discounts, taxes):
path: saleor/checkout/utils.py
text: @user1:
This function is almost 80 lines long, can we introduce some type of helpers here?

@author:
You are right, it was hard to read. I added some helpers.

diff_hunk: @@ -57,16 +649,162 @@ def _get_product_or_category_voucher_discount_for_checkout(voucher, checkout):
     return get_product_or_category_voucher_discount(voucher, prices)
 
 
-def get_voucher_discount_for_checkout(voucher, checkout):
+def get_voucher_discount_for_cart(voucher, cart):
     """Calculate discount value depending on voucher and discount types.
 
     Raise NotApplicable if voucher of given type cannot be applied.
     """
     if voucher.type == VoucherType.VALUE:
-        return _get_value_voucher_discount_for_checkout(voucher, checkout)
+        return _get_value_voucher_discount_for_cart(voucher, cart)
     if voucher.type == VoucherType.SHIPPING:
-        return _get_shipping_voucher_discount_for_checkout(voucher, checkout)
+        return _get_shipping_voucher_discount_for_cart(voucher, cart)
     if voucher.type in (VoucherType.PRODUCT, VoucherType.CATEGORY):
-        return _get_product_or_category_voucher_discount_for_checkout(
-            voucher, checkout)
+        return _get_product_or_category_voucher_discount_for_cart(
+            voucher, cart)
     raise NotImplementedError('Unknown discount type')
+
+
+def get_voucher_for_cart(cart, vouchers=None):
+    """Return voucher with voucher code saved in cart if active or None."""
+    if cart.voucher_code is not None:
+        if vouchers is None:
+            vouchers = Voucher.objects.active(date=date.today())
+        try:
+            return vouchers.get(code=cart.voucher_code)
+        except Voucher.DoesNotExist:
+            return None
+    return None
+
+
+def recalculate_cart_discount(cart):
+    """Recalculate `cart.discount` based on the voucher.
+
+    Will clear both voucher and discount if the discount is no longer
+    applicable.
+    """
+    voucher = get_voucher_for_cart(cart)
+    if voucher is not None:
+        try:
+            cart.discount_amount = get_voucher_discount_for_cart(voucher, cart)
+            cart.discount_name = voucher.name
+            cart.save(update_fields=['discount_amount', 'discount_name'])
+        except NotApplicable:
+            remove_voucher_from_cart(cart)
+    else:
+        remove_voucher_from_cart(cart)
+
+
+def remove_voucher_from_cart(cart):
+    """Remove voucher data from cart."""
+    cart.voucher_code = None
+    cart.discount_name = None
+    cart.discount_amount = ZERO_MONEY
+    cart.save(update_fields=[
+        'voucher_code', 'discount_name', 'discount_amount'])
+
+
+def get_taxes_for_cart(cart, default_taxes):
+    """Return taxes (if handled) due to shipping address or default one."""
+    if not settings.VATLAYER_ACCESS_KEY:
+        return None
+
+    if cart.shipping_address:
+        return get_taxes_for_country(cart.shipping_address.country)
+
+    return default_taxes
+
+
+def check_shipping_method(cart):
+    """Check if shipping method is valid for address and remove (if not)."""
+    country_code = cart.shipping_address.country.code
+    valid_methods = ShippingMethodCountry.objects.select_related(
+        'shipping_method').unique_for_country_code(country_code)
+    if cart.shipping_method not in valid_methods:
+        cart.shipping_method = None
+        cart.save()
+        return False
+    return True
+
+
+@transaction.atomic
+def create_order(cart, tracking_code, discounts, taxes):
+    """Create an order from the cart.
+
+    Each order will get a private copy of both the billing and the shipping
+    address (if shipping).
+
+    If any of the addresses is new and the user is logged in the address
+    will also get saved to that user's address book.
+
+    Current user's language is saved in the order so we can later determine
+    which language to use when sending email.
+    """
+    # FIXME: save locale along with the language
+    from ..order.utils import add_variant_to_order
+
+    voucher = get_voucher_for_cart(
+        cart, vouchers=Voucher.objects.active(
+            date=date.today()).select_for_update())
+
+    if cart.voucher_code and not voucher:
+        # Voucher expired in meantime, abort order placement
+        return None
+
+    billing_address = cart.billing_address
+
+    if cart.is_shipping_required():
+        shipping_address = cart.shipping_address
+        shipping_method = cart.shipping_method
+        shipping_method_name = smart_text(shipping_method)
+    else:
+        shipping_address = None
+        shipping_method = None
+        shipping_method_name = None
+
+    if cart.user:
+        store_user_address(cart.user, billing_address, AddressType.BILLING)
+
+        if cart.user.addresses.filter(pk=billing_address.pk).exists():
+            billing_address = billing_address.get_copy()
+
+        if cart.is_shipping_required():
+            store_user_address(
+                cart.user, shipping_address, AddressType.SHIPPING)
+
+            if cart.user.addresses.filter(pk=shipping_address.pk).exists():
+                shipping_address = shipping_address.get_copy()
+
+    order_data = {
+        'language_code': get_language(),
+        'billing_address': billing_address,
+        'shipping_address': shipping_address,
+        'tracking_client_id': tracking_code,
+        'shipping_method': shipping_method,
+        'shipping_method_name': shipping_method_name,
+        'shipping_price': cart.get_shipping_price(taxes),
+        'total': cart.get_total(taxes=taxes)}
+
+    if cart.user:
+        order_data['user'] = cart.user
+        order_data['user_email'] = cart.user.email
+    else:
+        order_data['user_email'] = cart.email
+
+    if voucher:
+        order_data['voucher'] = voucher
+        order_data['discount_amount'] = cart.discount_amount
+        order_data['discount_name'] = cart.discount_name
+
+    order = Order.objects.create(**order_data)
+
+    for line in cart:
+        add_variant_to_order(
+            order, line.variant, line.quantity, discounts, taxes)
+
+    if voucher:
path: saleor/checkout/utils.py
text: @user1:
Can we merge with above check?

@author:
Yes, I don't see anything against that.