Features: 1) Add BulkOrderAction mutation to handle bulk addition/removal of order products; 2) Introduce bulk_add_order_products and bulk_remove_order_products endpoints in viewset; 3) Add BulkAddOrderProductsSerializer and BulkRemoveOrderProductsSerializer.
Fixes: 1) Update `remove_product` model method to handle zero quantity removal; 2) Correct permission check in order product removal. Extra: 1) Add `autocomplete_fields` for products in admin; 2) Enhance docs with bulk add/remove schemas; 3) Various code refactorings and minor tweaks for improved maintainability.
This commit is contained in:
parent
6e5a008802
commit
4e269dc801
8 changed files with 138 additions and 6 deletions
|
|
@ -331,6 +331,7 @@ class PromoCodeAdmin(BasicModelAdmin):
|
|||
class PromotionAdmin(BasicModelAdmin, TabbedTranslationAdmin):
|
||||
list_display = ("name", "discount_percent", "modified")
|
||||
search_fields = ("name",)
|
||||
autocomplete_fields = ("products",)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ from core.serializers import (
|
|||
AttributeSimpleSerializer,
|
||||
AttributeValueDetailSerializer,
|
||||
AttributeValueSimpleSerializer,
|
||||
BulkAddOrderProductsSerializer,
|
||||
BulkAddWishlistProductSerializer,
|
||||
BulkRemoveOrderProductsSerializer,
|
||||
BulkRemoveWishlistProductSerializer,
|
||||
BuyOrderSerializer,
|
||||
BuyUnregisteredOrderSerializer,
|
||||
|
|
@ -196,12 +198,24 @@ ORDER_SCHEMA = {
|
|||
request=AddOrderProductSerializer,
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
|
||||
),
|
||||
"bulk_add_order_products": extend_schema(
|
||||
summary=_("add a list of products to order, quantities will not count"),
|
||||
description=_("adds a list of products to an order using the provided `product_uuid` and `attributes`."),
|
||||
request=BulkAddOrderProductsSerializer,
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
|
||||
),
|
||||
"remove_order_product": extend_schema(
|
||||
summary=_("remove product from order"),
|
||||
description=_("removes a product from an order using the provided `product_uuid` and `attributes`."),
|
||||
request=RemoveOrderProductSerializer,
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
|
||||
),
|
||||
"bulk_remove_order_products": extend_schema(
|
||||
summary=_("remove product from order, quantities will not count"),
|
||||
description=_("removes a list of products from an order using the provided `product_uuid` and `attributes`"),
|
||||
request=BulkRemoveOrderProductsSerializer,
|
||||
responses={status.HTTP_200_OK: OrderDetailSerializer, **BASE_ERRORS},
|
||||
),
|
||||
}
|
||||
|
||||
WISHLIST_SCHEMA = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,14 @@ from graphene_django.utils import camelize
|
|||
|
||||
from core.elasticsearch import process_query
|
||||
from core.graphene import BaseMutation
|
||||
from core.graphene.object_types import AddressType, OrderType, ProductType, SearchResultsType, WishlistType
|
||||
from core.graphene.object_types import (
|
||||
AddressType,
|
||||
BulkActionOrderProductInput,
|
||||
OrderType,
|
||||
ProductType,
|
||||
SearchResultsType,
|
||||
WishlistType,
|
||||
)
|
||||
from core.models import Address, Category, Order, Product, Wishlist
|
||||
from core.utils import format_attributes, is_url_safe
|
||||
from core.utils.caching import web_cache
|
||||
|
|
@ -221,6 +228,52 @@ class BuyOrder(BaseMutation):
|
|||
raise Http404(_(f"order {order_uuid} not found"))
|
||||
|
||||
|
||||
class BulkOrderAction(BaseMutation):
|
||||
class Meta:
|
||||
description = _("perform an action on a list of products in the order")
|
||||
|
||||
class Arguments:
|
||||
order_uuid = UUID(required=False)
|
||||
order_hr_id = String(required=False)
|
||||
action = String(required=True, description=_("remove/add"))
|
||||
products = List(BulkActionOrderProductInput, required=True)
|
||||
|
||||
order = Field(OrderType, required=False)
|
||||
|
||||
@staticmethod
|
||||
def mutate(
|
||||
_parent,
|
||||
info,
|
||||
action,
|
||||
products,
|
||||
order_uuid=None,
|
||||
order_hr_id=None,
|
||||
):
|
||||
if not any([order_uuid, order_hr_id]) or all([order_uuid, order_hr_id]):
|
||||
raise BadRequest(_("please provide either order_uuid or order_hr_id - mutually exclusive"))
|
||||
user = info.context.user
|
||||
try:
|
||||
order = None
|
||||
|
||||
if order_uuid:
|
||||
order = Order.objects.get(user=user, uuid=order_uuid)
|
||||
elif order_hr_id:
|
||||
order = Order.objects.get(user=user, human_readable_id=order_hr_id)
|
||||
|
||||
match action:
|
||||
case "add":
|
||||
order = order.bulk_add_products(products)
|
||||
case "remove":
|
||||
order = order.bulk_remove_products(products)
|
||||
case _:
|
||||
raise BadRequest(_("action must be either add or remove"))
|
||||
|
||||
return BulkOrderAction(order=order)
|
||||
|
||||
except Order.DoesNotExist:
|
||||
raise Http404(_(f"order {order_uuid} not found"))
|
||||
|
||||
|
||||
class BuyUnregisteredOrder(BaseMutation):
|
||||
class Meta:
|
||||
description = _("purchase an order without account creation")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.core.cache import cache
|
|||
from django.db.models import Max, Min, QuerySet
|
||||
from django.db.models.functions import Length
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from graphene import UUID, Field, Float, Int, List, NonNull, ObjectType, String, relay
|
||||
from graphene import UUID, Field, Float, InputObjectType, Int, List, NonNull, ObjectType, String, relay
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
|
@ -508,3 +508,8 @@ class SearchResultsType(ObjectType):
|
|||
categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType)
|
||||
brands = List(description=_("products search results"), of_type=SearchBrandsResultsType)
|
||||
posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)
|
||||
|
||||
|
||||
class BulkActionOrderProductInput(InputObjectType):
|
||||
id = UUID(required=True)
|
||||
attributes = GenericScalar(required=False)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from core.graphene.mutations import (
|
|||
AddOrderProduct,
|
||||
AddWishlistProduct,
|
||||
AutocompleteAddress,
|
||||
BulkOrderAction,
|
||||
BuyOrder,
|
||||
BuyProduct,
|
||||
BuyWishlist,
|
||||
|
|
@ -294,6 +295,7 @@ class Mutation(ObjectType):
|
|||
remove_all_order_products = RemoveAllOrderProducts.Field()
|
||||
remove_order_products_of_a_kind = RemoveOrderProductsOfAKind.Field()
|
||||
buy_order = BuyOrder.Field()
|
||||
bulk_order_action = BulkOrderAction.Field()
|
||||
deposit = Deposit.Field()
|
||||
obtain_jwt_token = ObtainJSONWebToken.Field()
|
||||
refresh_jwt_token = RefreshJSONWebToken.Field()
|
||||
|
|
|
|||
|
|
@ -546,7 +546,7 @@ class Order(NiceModel):
|
|||
def total_quantity(self) -> int:
|
||||
return sum([op.quantity for op in self.order_products.all()])
|
||||
|
||||
def add_product(self, product_uuid: str | None = None, attributes: list = list):
|
||||
def add_product(self, product_uuid: str | None = None, attributes: list = list, update_quantity: bool = True):
|
||||
if self.status not in ["PENDING", "MOMENTAL"]:
|
||||
raise ValueError(_("you cannot add products to an order that is not a pending one"))
|
||||
try:
|
||||
|
|
@ -568,7 +568,7 @@ class Order(NiceModel):
|
|||
attributes=json.dumps(attributes),
|
||||
defaults={"quantity": 1, "buy_price": product.price},
|
||||
)
|
||||
if not is_created:
|
||||
if not is_created and update_quantity:
|
||||
if product.quantity < order_product.quantity + 1:
|
||||
raise BadRequest(_("you cannot add more products than available in stock"))
|
||||
order_product.quantity += 1
|
||||
|
|
@ -581,12 +581,15 @@ class Order(NiceModel):
|
|||
name = "Product"
|
||||
raise Http404(_(f"{name} does not exist: {product_uuid}"))
|
||||
|
||||
def remove_product(self, product_uuid: str | None = None, attributes: dict = dict):
|
||||
def remove_product(self, product_uuid: str | None = None, attributes: dict = dict, zero_quantity: bool = False):
|
||||
if self.status != "PENDING":
|
||||
raise ValueError(_("you cannot remove products from an order that is not a pending one"))
|
||||
try:
|
||||
product = Product.objects.get(uuid=product_uuid)
|
||||
order_product = self.order_products.get(product=product, order=self)
|
||||
if zero_quantity:
|
||||
order_product.delete()
|
||||
return self
|
||||
if order_product.quantity == 1:
|
||||
self.order_products.remove(order_product)
|
||||
order_product.delete()
|
||||
|
|
@ -778,6 +781,16 @@ class Order(NiceModel):
|
|||
self.status = "FINISHED"
|
||||
self.save()
|
||||
|
||||
def bulk_add_products(self, products: list):
|
||||
for product in products:
|
||||
self.add_product(product.get("uuid"), attributes=product.get("attributes"), update_quantity=False)
|
||||
return self
|
||||
|
||||
def bulk_remove_products(self, products: list):
|
||||
for product in products:
|
||||
self.remove_product(product.get("uuid"), attributes=product.get("attributes"), zero_quantity=True)
|
||||
return self
|
||||
|
||||
|
||||
class OrderProduct(NiceModel):
|
||||
is_publicly_visible = False
|
||||
|
|
|
|||
|
|
@ -55,11 +55,19 @@ class AddOrderProductSerializer(Serializer):
|
|||
attributes = JSONField(required=False, default=dict)
|
||||
|
||||
|
||||
class BulkAddOrderProductsSerializer(Serializer):
|
||||
products = ListField(child=AddOrderProductSerializer(), required=True)
|
||||
|
||||
|
||||
class RemoveOrderProductSerializer(Serializer):
|
||||
product_uuid = CharField(required=True)
|
||||
attributes = JSONField(required=False, default=dict)
|
||||
|
||||
|
||||
class BulkRemoveOrderProductsSerializer(Serializer):
|
||||
products = ListField(child=RemoveOrderProductSerializer(), required=True)
|
||||
|
||||
|
||||
class AddWishlistProductSerializer(Serializer):
|
||||
product_uuid = CharField(required=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ from core.serializers import (
|
|||
AttributeValueSimpleSerializer,
|
||||
BrandDetailSerializer,
|
||||
BrandSimpleSerializer,
|
||||
BulkAddOrderProductsSerializer,
|
||||
BulkAddWishlistProductSerializer,
|
||||
BulkRemoveOrderProductsSerializer,
|
||||
BulkRemoveWishlistProductSerializer,
|
||||
BuyOrderSerializer,
|
||||
BuyUnregisteredOrderSerializer,
|
||||
|
|
@ -305,7 +307,7 @@ class OrderViewSet(EvibesViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = Order.objects.get(uuid=kwargs.get("pk"))
|
||||
if not (request.user.has_perm("core.add_orderproduct") or request.user == order.user):
|
||||
if not (request.user.has_perm("core.delete_orderproduct") or request.user == order.user):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.remove_product(
|
||||
|
|
@ -317,6 +319,40 @@ class OrderViewSet(EvibesViewSet):
|
|||
except Order.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="bulk_add_order_products")
|
||||
def bulk_add_order_products(self, request, *_args, **kwargs):
|
||||
serializer = BulkAddOrderProductsSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = Order.objects.get(uuid=kwargs.get("pk"))
|
||||
if not (request.user.has_perm("core.add_orderproduct") or request.user == order.user):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.bulk_add_products(
|
||||
products=serializer.validated_data.get("products"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
except Order.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="bulk_remove_order_products")
|
||||
def bulk_remove_order_products(self, request, *_args, **kwargs):
|
||||
serializer = BulkRemoveOrderProductsSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
order = Order.objects.get(uuid=kwargs.get("pk"))
|
||||
if not (request.user.has_perm("core.delete_orderproduct") or request.user == order.user):
|
||||
raise PermissionDenied(permission_denied_message)
|
||||
|
||||
order = order.bulk_remove_products(
|
||||
products=serializer.validated_data.get("products"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_200_OK, data=OrderDetailSerializer(order).data)
|
||||
except Order.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class OrderProductViewSet(EvibesViewSet):
|
||||
queryset = OrderProduct.objects.all()
|
||||
|
|
|
|||
Loading…
Reference in a new issue