Skip to content

Commit

Permalink
Remove quantity field from checkout (#7296)
Browse files Browse the repository at this point in the history
* Remove checkoutline when variant/product was deleted

* cleanup
  • Loading branch information
d-wysocki committed May 5, 2021
1 parent d11abd6 commit afa1c1c
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 86 deletions.
17 changes: 17 additions & 0 deletions saleor/checkout/migrations/0034_remove_checkout_quantity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-04-28 07:01

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("checkout", "0033_checkout_language_code"),
]

operations = [
migrations.RemoveField(
model_name="checkout",
name="quantity",
),
]
4 changes: 0 additions & 4 deletions saleor/checkout/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class Checkout(ModelWithMetadata):
)
email = models.EmailField()
token = models.UUIDField(primary_key=True, default=uuid4, editable=False)
quantity = models.PositiveIntegerField(default=0)
channel = models.ForeignKey(
Channel,
related_name="checkouts",
Expand Down Expand Up @@ -98,9 +97,6 @@ class Meta(ModelWithMetadata.Meta):
(CheckoutPermissions.MANAGE_CHECKOUTS.codename, "Manage checkouts"),
)

def __repr__(self):
return "Checkout(quantity=%s)" % (self.quantity,)

def __iter__(self):
return iter(self.lines.all())

Expand Down
18 changes: 6 additions & 12 deletions saleor/checkout/tests/test_cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ...product.models import Category
from .. import calculations, utils
from ..models import Checkout
from ..utils import add_variant_to_checkout
from ..utils import add_variant_to_checkout, calculate_checkout_quantity


@pytest.fixture()
Expand Down Expand Up @@ -35,11 +35,12 @@ def test_adding_same_variant(checkout, product):
checkout_info = fetch_checkout_info(checkout, [], [], get_plugins_manager())
add_variant_to_checkout(checkout_info, variant, 1)
add_variant_to_checkout(checkout_info, variant, 2)
lines = fetch_checkout_lines(checkout)
checkout_quantity = calculate_checkout_quantity(lines)
assert checkout.lines.count() == 1
assert checkout.quantity == 3
assert checkout_quantity == 3
subtotal = TaxedMoney(Money("30.00", "USD"), Money("30.00", "USD"))
manager = get_plugins_manager()
lines = fetch_checkout_lines(checkout)
checkout_info = fetch_checkout_info(checkout, lines, [], manager)
manager = get_plugins_manager()
assert (
Expand All @@ -58,8 +59,9 @@ def test_replacing_same_variant(checkout, product):
checkout_info = fetch_checkout_info(checkout, [], [], get_plugins_manager())
add_variant_to_checkout(checkout_info, variant, 1, replace=True)
add_variant_to_checkout(checkout_info, variant, 2, replace=True)
lines = fetch_checkout_lines(checkout)
assert checkout.lines.count() == 1
assert checkout.quantity == 2
assert calculate_checkout_quantity(lines) == 2


def test_adding_invalid_quantity(checkout, product):
Expand Down Expand Up @@ -243,14 +245,6 @@ def test_get_prices_of_discounted_specific_product_all_products(
assert prices == excepted_value


def test_checkout_repr():
checkout = Checkout()
assert repr(checkout) == "Checkout(quantity=0)"

checkout.quantity = 1
assert repr(checkout) == "Checkout(quantity=1)"


def test_checkout_line_repr(product, checkout_with_single_item):
variant = product.variants.get()
line = checkout_with_single_item.lines.first()
Expand Down
39 changes: 28 additions & 11 deletions saleor/checkout/tests/test_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
from ...plugins.manager import get_plugins_manager
from ...shipping.models import ShippingZone
from .. import AddressType, calculations
from ..fetch import CheckoutInfo, fetch_checkout_info, fetch_checkout_lines
from ..fetch import (
CheckoutInfo,
CheckoutLineInfo,
fetch_checkout_info,
fetch_checkout_lines,
)
from ..models import Checkout
from ..utils import (
add_voucher_to_checkout,
calculate_checkout_quantity,
cancel_active_payments,
change_billing_address_in_checkout,
change_shipping_address_in_checkout,
Expand Down Expand Up @@ -91,25 +97,25 @@ def test_last_change_update_foregin_key(checkout, shipping_method):


@pytest.mark.parametrize(
"total, min_spent_amount, total_quantity, min_checkout_items_quantity, "
"total, min_spent_amount, min_checkout_items_quantity, "
"discount_value, discount_value_type, expected_value",
[
(20, 20, 2, 2, 50, DiscountValueType.PERCENTAGE, 10),
(20, None, 2, None, 50, DiscountValueType.PERCENTAGE, 10),
(20, 20, 2, 2, 5, DiscountValueType.FIXED, 5),
(20, None, 2, None, 5, DiscountValueType.FIXED, 5),
(20, 20, 2, 50, DiscountValueType.PERCENTAGE, 10),
(20, None, None, 50, DiscountValueType.PERCENTAGE, 10),
(20, 20, 2, 5, DiscountValueType.FIXED, 5),
(20, None, None, 5, DiscountValueType.FIXED, 5),
],
)
def test_get_discount_for_checkout_value_voucher(
total,
min_spent_amount,
total_quantity,
min_checkout_items_quantity,
discount_value,
discount_value_type,
expected_value,
monkeypatch,
channel_USD,
checkout_with_items,
):
voucher = Voucher.objects.create(
code="unique",
Expand All @@ -123,7 +129,7 @@ def test_get_discount_for_checkout_value_voucher(
discount=Money(discount_value, channel_USD.currency_code),
min_spent_amount=(min_spent_amount if min_spent_amount is not None else None),
)
checkout = Mock(spec=Checkout, quantity=total_quantity, channel=channel_USD)
checkout = Mock(spec=checkout_with_items, channel=channel_USD)
subtotal = TaxedMoney(Money(total, "USD"), Money(total, "USD"))
monkeypatch.setattr(
"saleor.checkout.utils.calculations.checkout_subtotal",
Expand All @@ -143,9 +149,20 @@ def test_get_discount_for_checkout_value_voucher(
shipping_method_channel_listings=None,
valid_shipping_methods=[],
)
lines = [
CheckoutLineInfo(
line=line,
channel_listing=line.variant.product.channel_listings.first(),
collections=[],
product=line.variant.product,
variant=line.variant,
product_type=line.variant.product.product_type,
)
for line in checkout_with_items.lines.all()
]
manager = get_plugins_manager()
discount = get_voucher_discount_for_checkout(
manager, voucher, checkout_info, [], None, []
manager, voucher, checkout_info, lines, None, []
)
assert discount == Money(expected_value, "USD")

Expand All @@ -156,12 +173,12 @@ def test_get_voucher_discount_for_checkout_voucher_validation(
):
manager = get_plugins_manager()
lines = fetch_checkout_lines(checkout_with_voucher)
quantity = calculate_checkout_quantity(lines)
checkout_info = fetch_checkout_info(checkout_with_voucher, lines, [], manager)
manager = get_plugins_manager()
address = checkout_with_voucher.shipping_address
get_voucher_discount_for_checkout(manager, voucher, checkout_info, lines, address)
subtotal = manager.calculate_checkout_subtotal(checkout_info, lines, address, [])
quantity = checkout_with_voucher.quantity
customer_email = checkout_with_voucher.get_customer_email()
mock_validate_voucher.assert_called_once_with(
voucher, subtotal.gross, quantity, customer_email, checkout_with_voucher.channel
Expand Down Expand Up @@ -201,7 +218,7 @@ def test_get_discount_for_checkout_entire_order_voucher_not_applicable(
discount=Money(discount_value, channel_USD.currency_code),
min_spent_amount=(min_spent_amount if min_spent_amount is not None else None),
)
checkout = Mock(spec=Checkout, quantity=total_quantity, channel=channel_USD)
checkout = Mock(spec=Checkout, channel=channel_USD)
subtotal = TaxedMoney(Money(total, "USD"), Money(total, "USD"))
monkeypatch.setattr(
"saleor.checkout.utils.calculations.checkout_subtotal",
Expand Down
47 changes: 32 additions & 15 deletions saleor/checkout/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Checkout-related utility functions."""
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple

import graphene
from django.core.exceptions import ValidationError
from django.db.models import Sum
from django.db.models import Count, F, OuterRef, Subquery, Sum
from django.db.models.functions import Coalesce
from django.utils import timezone
from prices import Money

Expand Down Expand Up @@ -117,10 +119,13 @@ def add_variant_to_checkout(
line.quantity = new_quantity
line.save(update_fields=["quantity"])

checkout = update_checkout_quantity(checkout)
return checkout


def calculate_checkout_quantity(lines: Iterable["CheckoutLineInfo"]):
return sum([line_info.line.quantity for line_info in lines])


def add_variants_to_checkout(checkout, variants, quantities, channel_slug):
"""Add variants to checkout.
Expand Down Expand Up @@ -151,19 +156,6 @@ def add_variants_to_checkout(checkout, variants, quantities, channel_slug):
CheckoutLine(checkout=checkout, variant=variant, quantity=quantity)
)
checkout.lines.bulk_create(lines)
checkout = update_checkout_quantity(checkout)
return checkout


def update_checkout_quantity(checkout):
"""Update the total quantity in checkout."""
total_lines = checkout.lines.aggregate(total_quantity=Sum("quantity"))[
"total_quantity"
]
if not total_lines:
total_lines = 0
checkout.quantity = total_lines
checkout.save(update_fields=["quantity"])
return checkout


Expand Down Expand Up @@ -651,3 +643,28 @@ def is_shipping_required(lines: Iterable["CheckoutLineInfo"]):
return any(
line_info.product.product_type.is_shipping_required for line_info in lines
)


def validate_variants_in_checkout_lines(lines: Iterable["CheckoutLineInfo"]):
variants_listings_map = {line.variant.id: line.channel_listing for line in lines}

not_available_variants = [
variant_id
for variant_id, channel_listing in variants_listings_map.items()
if channel_listing is None or channel_listing.price is None
]
if not_available_variants:
not_available_variants_ids = {
graphene.Node.to_global_id("ProductVariant", pk)
for pk in not_available_variants
}
error_code = CheckoutErrorCode.UNAVAILABLE_VARIANT_IN_CHANNEL
raise ValidationError(
{
"lines": ValidationError(
"Cannot add lines with unavailable variants.",
code=error_code, # type: ignore
params={"variants": not_available_variants_ids},
)
}
)
6 changes: 4 additions & 2 deletions saleor/discount/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ def validate_voucher_for_checkout(
lines: Iterable["CheckoutLineInfo"],
discounts: Optional[Iterable[DiscountInfo]],
):
checkout = checkout_info.checkout
from ..checkout.utils import calculate_checkout_quantity

quantity = calculate_checkout_quantity(lines)
address = checkout_info.shipping_address or checkout_info.billing_address
subtotal = calculations.checkout_subtotal(
manager=manager,
Expand All @@ -130,7 +132,7 @@ def validate_voucher_for_checkout(
validate_voucher(
voucher,
subtotal.gross,
checkout.quantity,
quantity,
customer_email,
checkout_info.channel,
)
Expand Down
15 changes: 5 additions & 10 deletions saleor/graphql/checkout/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
add_promo_code_to_checkout,
add_variant_to_checkout,
add_variants_to_checkout,
calculate_checkout_quantity,
change_billing_address_in_checkout,
change_shipping_address_in_checkout,
get_user_checkout,
is_shipping_required,
recalculate_checkout_discount,
remove_promo_code_from_checkout,
update_checkout_quantity,
validate_variants_in_checkout_lines,
)
from ...core import analytics
from ...core.exceptions import InsufficientStock, PermissionDenied, ProductNotPublished
Expand Down Expand Up @@ -92,8 +93,9 @@ def update_checkout_shipping_method_if_invalid(
checkout_info: "CheckoutInfo", lines: Iterable[CheckoutLineInfo]
):
checkout = checkout_info.checkout
quantity = calculate_checkout_quantity(lines)
# remove shipping method when empty checkout
if checkout.quantity == 0 or not is_shipping_required(lines):
if quantity == 0 or not is_shipping_required(lines):
checkout.shipping_method = None
checkout_info.shipping_method = None
checkout_info.shipping_method_channel_listings = None
Expand Down Expand Up @@ -538,7 +540,6 @@ def perform_mutation(cls, _root, info, checkout_id, line_id):
checkout, lines, info.context.discounts, manager
)
update_checkout_shipping_method_if_invalid(checkout_info, lines)
update_checkout_quantity(checkout)
recalculate_checkout_discount(
manager, checkout_info, lines, info.context.discounts
)
Expand Down Expand Up @@ -674,7 +675,6 @@ def perform_mutation(cls, _root, info, checkout_id, shipping_address):

discounts = info.context.discounts
manager = info.context.plugins
lines = fetch_checkout_lines(checkout)
checkout_info = fetch_checkout_info(checkout, lines, discounts, manager)

country = get_user_country_context(
Expand Down Expand Up @@ -936,12 +936,7 @@ def perform_mutation(cls, _root, info, checkout_id, store_source, **data):

manager = info.context.plugins
lines = fetch_checkout_lines(checkout)
variants_id = {line.variant.id for line in lines}
validate_variants_available_in_channel(
variants_id,
checkout.channel,
CheckoutErrorCode.UNAVAILABLE_VARIANT_IN_CHANNEL,
)
validate_variants_in_checkout_lines(lines)
checkout_info = fetch_checkout_info(
checkout, lines, info.context.discounts, manager
)
Expand Down

0 comments on commit afa1c1c

Please sign in to comment.