Skip to content

Commit

Permalink
Fix for fulfilling an order when product quantity equals allocated qu…
Browse files Browse the repository at this point in the history
…antity (#6333)

* Fix for fulfilling an order when product quantity equals allocated quantity

* Add tests, update the CHANGELOG.md, modify schema objects

* Style improvements

* Add data loader, add permission decorator, style changes

* Changes after code review
  • Loading branch information
Grzegorz Derdak committed Oct 30, 2020
1 parent 60dab6a commit c1e0c1c
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -77,6 +77,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add metadata to page model - #6292 by @dominik-zeglen
- Fix for unnecesary attributes validation while updating simple product - #6300 by @GrzegorzDerdak
- Include order line total price to webhook payload - #6354 by @korycins
- Fix for fulfilling an order when product quantity equals allocated quantity - #6333 by @GrzegorzDerdak

## 2.10.2

Expand Down
17 changes: 17 additions & 0 deletions saleor/graphql/order/dataloaders.py
@@ -0,0 +1,17 @@
from collections import defaultdict

from ...warehouse.models import Allocation
from ..core.dataloaders import DataLoader


class AllocationsByOrderLineIdLoader(DataLoader):
context_key = "allocations_by_orderline_id"

def batch_load(self, keys):
allocations = Allocation.objects.filter(order_line__pk__in=keys)
order_lines_to_allocations = defaultdict(list)

for allocation in allocations:
order_lines_to_allocations[allocation.order_line_id].append(allocation)

return [order_lines_to_allocations[order_line_id] for order_line_id in keys]
80 changes: 73 additions & 7 deletions saleor/graphql/order/tests/test_order.py
Expand Up @@ -101,6 +101,13 @@ def test_orderline_query(staff_api_client, permission_manage_orders, fulfilled_o
id
}
quantity
allocations {
id
quantity
warehouse {
id
}
}
unitPrice {
currency
gross {
Expand All @@ -126,23 +133,82 @@ def test_orderline_query(staff_api_client, permission_manage_orders, fulfilled_o
response = staff_api_client.post_graphql(query)
content = get_graphql_content(response)
order_data = content["data"]["orders"]["edges"][0]["node"]
assert order_data["lines"][0]["thumbnail"] is None
first_order_data_line = order_data["lines"][0]
variant_id = graphene.Node.to_global_id("ProductVariant", line.variant.pk)
assert order_data["lines"][0]["variant"]["id"] == variant_id
assert order_data["lines"][0]["quantity"] == line.quantity
assert order_data["lines"][0]["unitPrice"]["currency"] == line.unit_price.currency

assert first_order_data_line["thumbnail"] is None
assert first_order_data_line["variant"]["id"] == variant_id
assert first_order_data_line["quantity"] == line.quantity
assert first_order_data_line["unitPrice"]["currency"] == line.unit_price.currency

expected_unit_price = Money(
amount=str(order_data["lines"][0]["unitPrice"]["gross"]["amount"]),
amount=str(first_order_data_line["unitPrice"]["gross"]["amount"]),
currency="USD",
)
assert order_data["lines"][0]["totalPrice"]["currency"] == line.unit_price.currency
assert first_order_data_line["totalPrice"]["currency"] == line.unit_price.currency
assert expected_unit_price == line.unit_price.gross

expected_total_price = Money(
amount=str(order_data["lines"][0]["totalPrice"]["gross"]["amount"]),
amount=str(first_order_data_line["totalPrice"]["gross"]["amount"]),
currency="USD",
)
assert expected_total_price == line.unit_price.gross * line.quantity

allocation = line.allocations.first()
allocation_id = graphene.Node.to_global_id("Allocation", allocation.pk)
warehouse_id = graphene.Node.to_global_id(
"Warehouse", allocation.stock.warehouse.pk
)
assert first_order_data_line["allocations"] == [
{"id": allocation_id, "quantity": 0, "warehouse": {"id": warehouse_id}}
]


def test_order_line_with_allocations(
staff_api_client, permission_manage_orders, order_with_lines,
):
# given
order = order_with_lines
query = """
query OrdersQuery {
orders(first: 1) {
edges {
node {
lines {
id
allocations {
id
quantity
warehouse {
id
}
}
}
}
}
}
}
"""
staff_api_client.user.user_permissions.add(permission_manage_orders)

# when
response = staff_api_client.post_graphql(query)

# then
content = get_graphql_content(response)
lines = content["data"]["orders"]["edges"][0]["node"]["lines"]

for line in lines:
_, _id = graphene.Node.from_global_id(line["id"])
order_line = order.lines.get(pk=_id)
allocations_from_query = {
allocation["quantity"] for allocation in line["allocations"]
}
allocations_from_db = set(
order_line.allocations.values_list("quantity_allocated", flat=True)
)
assert allocations_from_query == allocations_from_db


def test_order_query(
staff_api_client, permission_manage_orders, fulfilled_order, shipping_zone
Expand Down
18 changes: 15 additions & 3 deletions saleor/graphql/order/types.py
Expand Up @@ -4,7 +4,7 @@

from ...core.anonymize import obfuscate_address, obfuscate_email
from ...core.exceptions import PermissionDenied
from ...core.permissions import AccountPermissions, OrderPermissions
from ...core.permissions import AccountPermissions, OrderPermissions, ProductPermissions
from ...core.taxes import display_gross_prices
from ...graphql.utils import get_user_or_app_from_context
from ...order import OrderStatus, models
Expand All @@ -18,15 +18,16 @@
from ..core.connection import CountableDjangoObjectType
from ..core.types.common import Image
from ..core.types.money import Money, TaxedMoney
from ..decorators import permission_required
from ..decorators import one_of_permissions_required, permission_required
from ..giftcard.types import GiftCard
from ..invoice.types import Invoice
from ..meta.deprecated.resolvers import resolve_meta, resolve_private_meta
from ..meta.types import ObjectWithMetadata
from ..payment.types import OrderAction, Payment, PaymentChargeStatusEnum
from ..product.types import ProductVariant
from ..shipping.types import ShippingMethod
from ..warehouse.types import Warehouse
from ..warehouse.types import Allocation, Warehouse
from .dataloaders import AllocationsByOrderLineIdLoader
from .enums import OrderEventsEmailsEnum, OrderEventsEnum
from .utils import validate_draft_order

Expand Down Expand Up @@ -255,6 +256,10 @@ class OrderLine(CountableDjangoObjectType):
translated_variant_name = graphene.String(
required=True, description="Variant name in the customer's language"
)
allocations = graphene.List(
graphene.NonNull(Allocation),
description="List of allocations across warehouses.",
)

class Meta:
description = "Represents order line of particular order."
Expand Down Expand Up @@ -299,6 +304,13 @@ def resolve_translated_product_name(root: models.OrderLine, _info):
def resolve_translated_variant_name(root: models.OrderLine, _info):
return root.translated_variant_name

@staticmethod
@one_of_permissions_required(
[ProductPermissions.MANAGE_PRODUCTS, OrderPermissions.MANAGE_ORDERS]
)
def resolve_allocations(root: models.OrderLine, info):
return AllocationsByOrderLineIdLoader(info.context).load(root.id)


class Order(CountableDjangoObjectType):
fulfillments = graphene.List(
Expand Down
7 changes: 7 additions & 0 deletions saleor/graphql/schema.graphql
Expand Up @@ -195,6 +195,12 @@ type AddressValidationData {
postalCodePrefix: String
}

type Allocation implements Node {
id: ID!
quantity: Int!
warehouse: Warehouse!
}

type App implements Node & ObjectWithMetadata {
id: ID!
name: String
Expand Down Expand Up @@ -3142,6 +3148,7 @@ type OrderLine implements Node {
variant: ProductVariant
translatedProductName: String!
translatedVariantName: String!
allocations: [Allocation!]
}

input OrderLineCreateInput {
Expand Down
29 changes: 29 additions & 0 deletions saleor/graphql/warehouse/types.py
Expand Up @@ -90,3 +90,32 @@ def resolve_quantity_allocated(root, *_args):
return root.allocations.aggregate(
quantity_allocated=Coalesce(Sum("quantity_allocated"), 0)
)["quantity_allocated"]


class Allocation(CountableDjangoObjectType):
quantity = graphene.Int(required=True, description="Quantity allocated for orders.")
warehouse = graphene.Field(
Warehouse, required=True, description="The warehouse were items were allocated."
)

class Meta:
description = "Represents allocation."
model = models.Allocation
interfaces = [graphene.relay.Node]
only_fields = ["id"]

@staticmethod
@one_of_permissions_required(
[ProductPermissions.MANAGE_PRODUCTS, OrderPermissions.MANAGE_ORDERS]
)
def resolve_warehouse(root, *_args):
return root.stock.warehouse

@staticmethod
@one_of_permissions_required(
[ProductPermissions.MANAGE_PRODUCTS, OrderPermissions.MANAGE_ORDERS]
)
def resolve_quantity(root, *_args):
return root.stock.allocations.aggregate(
quantity_allocated=Coalesce(Sum("quantity_allocated"), 0)
)["quantity_allocated"]

0 comments on commit c1e0c1c

Please sign in to comment.