ob-dj-store 0.0.11.3__py3-none-any.whl → 0.0.11.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. ob_dj_store/apis/stores/filters.py +33 -18
  2. ob_dj_store/apis/stores/rest/serializers/serializers.py +129 -53
  3. ob_dj_store/apis/stores/urls.py +3 -3
  4. ob_dj_store/apis/stores/views.py +182 -42
  5. ob_dj_store/core/stores/admin.py +8 -3
  6. ob_dj_store/core/stores/admin_inlines.py +4 -4
  7. ob_dj_store/core/stores/gateway/tap/models.py +1 -1
  8. ob_dj_store/core/stores/gateway/tap/utils.py +4 -4
  9. ob_dj_store/core/stores/managers.py +8 -6
  10. ob_dj_store/core/stores/migrations/0056_auto_20230213_2224.py +33 -0
  11. ob_dj_store/core/stores/migrations/0057_auto_20230214_1724.py +37 -0
  12. ob_dj_store/core/stores/migrations/0058_attributechoice_is_default.py +18 -0
  13. ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +41 -0
  14. ob_dj_store/core/stores/migrations/0060_alter_orderitem_product_variant.py +24 -0
  15. ob_dj_store/core/stores/migrations/0061_auto_20230223_1435.py +28 -0
  16. ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +43 -0
  17. ob_dj_store/core/stores/migrations/0063_alter_store_payment_methods.py +23 -0
  18. ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +24 -0
  19. ob_dj_store/core/stores/migrations/0065_auto_20230228_1932.py +34 -0
  20. ob_dj_store/core/stores/models/_favorite.py +4 -1
  21. ob_dj_store/core/stores/models/_order.py +4 -2
  22. ob_dj_store/core/stores/models/_payment.py +31 -11
  23. ob_dj_store/core/stores/models/_product.py +22 -13
  24. ob_dj_store/core/stores/models/_store.py +15 -6
  25. ob_dj_store/core/stores/models/_wallet.py +41 -8
  26. ob_dj_store/core/stores/receivers.py +79 -4
  27. ob_dj_store/core/stores/utils.py +12 -6
  28. ob_dj_store/utils/helpers.py +8 -2
  29. ob_dj_store/utils/utils.py +43 -3
  30. {ob_dj_store-0.0.11.3.dist-info → ob_dj_store-0.0.11.10.dist-info}/METADATA +1 -1
  31. {ob_dj_store-0.0.11.3.dist-info → ob_dj_store-0.0.11.10.dist-info}/RECORD +33 -23
  32. {ob_dj_store-0.0.11.3.dist-info → ob_dj_store-0.0.11.10.dist-info}/WHEEL +0 -0
  33. {ob_dj_store-0.0.11.3.dist-info → ob_dj_store-0.0.11.10.dist-info}/top_level.txt +0 -0
@@ -5,10 +5,12 @@ from django.utils.timezone import now
5
5
  from django_filters import rest_framework as filters
6
6
  from rest_framework.exceptions import ValidationError
7
7
 
8
+ from config import settings as store_settings
8
9
  from ob_dj_store.core.stores.models import (
9
10
  Category,
10
11
  Favorite,
11
12
  Order,
13
+ PaymentMethod,
12
14
  Product,
13
15
  ProductVariant,
14
16
  Store,
@@ -72,7 +74,6 @@ class StoreFilter(filters.FilterSet):
72
74
  class ProductFilter(filters.FilterSet):
73
75
  """Product filters"""
74
76
 
75
- store = filters.CharFilter(method="by_store")
76
77
  category = filters.CharFilter(method="by_category")
77
78
 
78
79
  class Meta:
@@ -80,20 +81,9 @@ class ProductFilter(filters.FilterSet):
80
81
  fields = [
81
82
  "is_featured",
82
83
  "type",
83
- "store",
84
84
  "category",
85
85
  ]
86
86
 
87
- def by_store(self, queryset, name, value):
88
- return queryset.prefetch_related(
89
- Prefetch(
90
- "products",
91
- queryset=Product.objects.filter(
92
- product_variants__inventories__store=value
93
- ),
94
- )
95
- ).filter(product_variants__inventories__store=value)
96
-
97
87
  def by_category(self, queryset, name, value):
98
88
  return queryset.filter(category__name__iexact=value)
99
89
 
@@ -123,22 +113,25 @@ class CategoryFilter(filters.FilterSet):
123
113
 
124
114
  def by_store(self, queryset, name, value):
125
115
  return (
126
- queryset.prefetch_related(
116
+ queryset.filter(
117
+ subcategories__products__product_variants__inventories__store=value
118
+ )
119
+ .prefetch_related(
127
120
  Prefetch(
128
121
  "subcategories",
129
- queryset=Category.objects.exclude(
130
- products__isnull=True,
131
- ),
122
+ queryset=Category.objects.filter(
123
+ products__product_variants__inventories__store=value
124
+ ).distinct(),
132
125
  ),
133
126
  Prefetch(
134
127
  "subcategories__products",
135
128
  queryset=Product.objects.filter(
136
129
  product_variants__inventories__store=value,
137
130
  is_active=True,
138
- ),
131
+ ).distinct(),
139
132
  ),
133
+ "subcategories__products__images",
140
134
  )
141
- .filter(subcategories__products__product_variants__inventories__store=value)
142
135
  .distinct()
143
136
  )
144
137
 
@@ -184,3 +177,25 @@ class FavoriteFilter(filters.FilterSet):
184
177
  content_type=content_type,
185
178
  object_id__in=products_ids,
186
179
  )
180
+
181
+
182
+ class PaymentMethodFilter(filters.FilterSet):
183
+ store = filters.CharFilter(method="by_store")
184
+ is_digital = filters.BooleanFilter(method="by_digital")
185
+
186
+ class Meta:
187
+ models = PaymentMethod
188
+
189
+ def by_store(self, queryset, name, value):
190
+ return queryset.filter(stores=value)
191
+
192
+ def by_digital(self, queryset, name, value):
193
+ if value:
194
+ return queryset.filter(
195
+ payment_provider__in=[
196
+ store_settings.TAP_CREDIT_CARD,
197
+ store_settings.TAP_KNET,
198
+ store_settings.TAP_ALL,
199
+ ]
200
+ )
201
+ return queryset
@@ -1,4 +1,5 @@
1
1
  import calendar
2
+ import sys
2
3
  import typing
3
4
  from datetime import time, timedelta
4
5
 
@@ -38,6 +39,7 @@ from ob_dj_store.core.stores.models import (
38
39
  ShippingMethod,
39
40
  Store,
40
41
  Tax,
42
+ Wallet,
41
43
  )
42
44
  from ob_dj_store.core.stores.models._inventory import Inventory
43
45
  from ob_dj_store.core.stores.utils import distance
@@ -50,6 +52,7 @@ class AttributeChoiceSerializer(serializers.ModelSerializer):
50
52
  "id",
51
53
  "name",
52
54
  "price",
55
+ "is_default",
53
56
  )
54
57
 
55
58
 
@@ -68,19 +71,22 @@ class AttributeSerializer(serializers.ModelSerializer):
68
71
  class InventoryValidationMixin:
69
72
  def validate(self, attrs: typing.Dict) -> typing.Dict:
70
73
  validated_data = super().validate(attrs)
71
- if validated_data["product_variant"].has_inventory:
72
- if validated_data["quantity"] < 1:
73
- raise serializers.ValidationError(_("Quantity must be greater than 0."))
74
- # validate quantity in inventory
74
+ inventory = None
75
+ try:
75
76
  inventory = validated_data["product_variant"].inventories.get(
76
77
  store=validated_data["store"]
77
78
  )
78
- if not inventory.is_uncountable:
79
- stock_quantity = inventory.quantity
80
- if validated_data["quantity"] > stock_quantity:
81
- raise serializers.ValidationError(
82
- _("Quantity is greater than the stock quantity.")
83
- )
79
+ except:
80
+ raise serializers.ValidationError(_("Product has no inventory"))
81
+ if validated_data["quantity"] < 1:
82
+ raise serializers.ValidationError(_("Quantity must be greater than 0."))
83
+ # validate quantity in inventory
84
+ if not inventory.is_uncountable:
85
+ stock_quantity = inventory.quantity
86
+ if validated_data["quantity"] > stock_quantity:
87
+ raise serializers.ValidationError(
88
+ _("Quantity is greater than the stock quantity.")
89
+ )
84
90
  return validated_data
85
91
 
86
92
 
@@ -283,7 +289,7 @@ class OrderSerializer(serializers.ModelSerializer):
283
289
  if payment_method:
284
290
  if payment_method.payment_provider == store_settings.WALLET:
285
291
  try:
286
- wallet = user.wallets.get(country=attrs["store"].address.country)
292
+ wallet = user.wallets.get(currency=attrs["store"].currency)
287
293
  except ObjectDoesNotExist:
288
294
  raise serializers.ValidationError(
289
295
  {
@@ -397,6 +403,8 @@ class ProductAttributeSerializer(serializers.ModelSerializer):
397
403
  "is_mandatory",
398
404
  "attribute_choices",
399
405
  "type",
406
+ "min",
407
+ "max",
400
408
  )
401
409
 
402
410
 
@@ -427,6 +435,8 @@ class ProductVariantSerializer(serializers.ModelSerializer):
427
435
 
428
436
  class CartItemSerializer(InventoryValidationMixin, serializers.ModelSerializer):
429
437
  image = serializers.SerializerMethodField()
438
+ inventory_quantity = serializers.SerializerMethodField()
439
+ is_uncountable = serializers.SerializerMethodField()
430
440
 
431
441
  class Meta:
432
442
  model = CartItem
@@ -442,6 +452,8 @@ class CartItemSerializer(InventoryValidationMixin, serializers.ModelSerializer):
442
452
  "extra_infos",
443
453
  "attribute_choices_total_price",
444
454
  "image",
455
+ "inventory_quantity",
456
+ "is_uncountable",
445
457
  )
446
458
  extra_kwargs = {
447
459
  "store": {
@@ -449,6 +461,16 @@ class CartItemSerializer(InventoryValidationMixin, serializers.ModelSerializer):
449
461
  },
450
462
  }
451
463
 
464
+ def get_inventory_quantity(self, obj):
465
+ if obj.inventory:
466
+ return obj.inventory.quantity
467
+ return None
468
+
469
+ def get_is_uncountable(self, obj):
470
+ if obj.inventory:
471
+ return obj.inventory.is_uncountable
472
+ return None
473
+
452
474
  def validate(self, attrs: typing.Dict) -> typing.Dict:
453
475
  return super(CartItemSerializer, self).validate(attrs)
454
476
 
@@ -515,9 +537,16 @@ class CartSerializer(serializers.ModelSerializer):
515
537
  cart_item.save()
516
538
  return instance
517
539
 
540
+ def to_representation(self, instance):
541
+ data = super().to_representation(instance)
542
+ stores = Store.objects.filter(store_items__cart=instance)
543
+ data["store"] = StoreSerializer(stores, many=True, context=self.context).data
544
+ return data
545
+
518
546
 
519
547
  class ProductMediaSerializer(serializers.ModelSerializer):
520
- image_thumbnail = serializers.ImageField(read_only=True)
548
+ image_thumbnail_medium = serializers.ImageField(read_only=True)
549
+ image_thumbnail_small = serializers.ImageField(read_only=True)
521
550
 
522
551
  class Meta:
523
552
  model = ProductMedia
@@ -525,7 +554,8 @@ class ProductMediaSerializer(serializers.ModelSerializer):
525
554
  "id",
526
555
  "is_primary",
527
556
  "image",
528
- "image_thumbnail",
557
+ "image_thumbnail_small",
558
+ "image_thumbnail_medium",
529
559
  "order_value",
530
560
  )
531
561
 
@@ -574,8 +604,8 @@ class ProductSerializer(FavoriteMixin, serializers.ModelSerializer):
574
604
  return data
575
605
 
576
606
 
577
- class ProductListSerializer(ProductSerializer):
578
- product_variants = ProductVariantSerializer(many=True)
607
+ class ProductListSerializer(serializers.ModelSerializer):
608
+ product_images = ProductMediaSerializer(many=True, source="images")
579
609
 
580
610
  class Meta:
581
611
  model = Product
@@ -585,15 +615,14 @@ class ProductListSerializer(ProductSerializer):
585
615
  "slug",
586
616
  "description",
587
617
  "product_images",
588
- "product_variants",
589
618
  "type",
590
- "default_variant",
591
619
  )
592
620
 
593
621
 
594
622
  class SubCategorySerializer(serializers.ModelSerializer):
595
623
  products = ProductListSerializer(many=True)
596
- image_thumbnail = serializers.ImageField(read_only=True)
624
+ image_thumbnail_medium = serializers.ImageField(read_only=True)
625
+ image_thumbnail_small = serializers.ImageField(read_only=True)
597
626
 
598
627
  class Meta:
599
628
  model = Category
@@ -604,12 +633,13 @@ class SubCategorySerializer(serializers.ModelSerializer):
604
633
  "is_active",
605
634
  "products",
606
635
  "image",
607
- "image_thumbnail",
636
+ "image_thumbnail_medium",
637
+ "image_thumbnail_small",
638
+ "parent",
608
639
  )
609
640
 
610
641
  def to_representation(self, instance):
611
642
  data = super().to_representation(instance)
612
- data["parent_id"] = instance.parent.id if instance.parent else None
613
643
  return data
614
644
 
615
645
 
@@ -627,6 +657,9 @@ class CategorySerializer(serializers.ModelSerializer):
627
657
  "is_active",
628
658
  "subcategories",
629
659
  "parent",
660
+ "image",
661
+ "image_thumbnail_medium",
662
+ "image_thumbnail_small",
630
663
  )
631
664
 
632
665
 
@@ -765,9 +798,7 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
765
798
 
766
799
  def get_is_closed(self, obj):
767
800
  current_time = now()
768
- current_op_hour = obj.opening_hours.filter(
769
- weekday=current_time.weekday() + 1
770
- ).first()
801
+ current_op_hour = obj.current_opening_hours
771
802
  if current_op_hour:
772
803
  return (
773
804
  not current_op_hour.from_hour
@@ -781,10 +812,11 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
781
812
  in_range_method = obj.shipping_methods.filter(
782
813
  type=ShippingMethod.ShippingType.DELIVERY
783
814
  ).exists()
784
- if user_location and obj.poly and in_range_method:
815
+ if not in_range_method:
816
+ return True
817
+ if user_location and obj.poly:
785
818
  long, lat = user_location.split(",")
786
819
  return obj.poly.contains(Point(float(long), float(lat)))
787
- return False
788
820
 
789
821
  def get_opening_hours(self, obj):
790
822
  opening_hours = sorted(
@@ -838,9 +870,7 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
838
870
  return PhoneContactSerializer(phone_contacts, many=True).data
839
871
 
840
872
  def get_current_day_opening_hours(self, obj):
841
- current_opening_hours = obj.opening_hours.filter(
842
- weekday=now().weekday() + 1
843
- ).first()
873
+ current_opening_hours = obj.current_opening_hours
844
874
  op_hour = {}
845
875
  if current_opening_hours:
846
876
  op_hour["from_hour"] = current_opening_hours.from_hour.strftime("%I:%M%p")
@@ -851,15 +881,7 @@ class StoreSerializer(FavoriteMixin, serializers.ModelSerializer):
851
881
  return op_hour
852
882
 
853
883
  def to_representation(self, instance):
854
- data = super().to_representation(instance)
855
- view = self.context.get("view")
856
- if view:
857
- if view.action != "retrieve":
858
- return data
859
- data["inventories"] = InventorySerializer(
860
- instance.inventories.all(), many=True
861
- ).data
862
- return data
884
+ return super().to_representation(instance)
863
885
 
864
886
 
865
887
  class PaymentMethodSerializer(serializers.ModelSerializer):
@@ -899,17 +921,27 @@ class StoreListSerializer(serializers.ModelSerializer):
899
921
 
900
922
 
901
923
  class GenericSerializer(serializers.Serializer):
924
+ """
925
+ A generic serializer that automatically selects the appropriate serializer
926
+ for a given instance.
927
+
928
+ The `to_representation` method uses the `get_serializer_for_instance` method
929
+ to determine which serializer to use for a given instance. If the serializer
930
+ class is found, it returns the representation of the instance using that
931
+ serializer. Otherwise, it raises a `NameError`.
932
+ """
933
+
902
934
  def to_representation(self, value):
903
935
  context = self.context
904
936
  serializer_class = self.get_serializer_for_instance(value)
905
937
  return serializer_class(context=context).to_representation(value)
906
938
 
907
939
  def get_serializer_for_instance(self, instance):
908
- serializer_name = type(instance).__name__ + "Serializer"
909
- try:
910
- return eval(serializer_name)
911
- except NameError:
912
- raise NameError(_(f"name '{serializer_name}' is not defined"))
940
+ serializer_class = instance.__class__.__name__ + "Serializer"
941
+ if hasattr(sys.modules[__name__], serializer_class):
942
+ return getattr(sys.modules[__name__], serializer_class)
943
+ else:
944
+ raise NameError(_(f"name '{serializer_class}' is not defined"))
913
945
 
914
946
 
915
947
  class FavoriteExtraSerializer(serializers.ModelSerializer):
@@ -945,7 +977,11 @@ class FavoriteSerializer(serializers.ModelSerializer):
945
977
  "extras",
946
978
  "object_id",
947
979
  "object_type",
980
+ "name",
948
981
  )
982
+ extra_kwargs = {
983
+ "name": {"required": True},
984
+ }
949
985
 
950
986
  def _lookup_validation(self, data):
951
987
  content_type = ContentType.objects.get_for_model(type(data["content_object"]))
@@ -956,6 +992,7 @@ class FavoriteSerializer(serializers.ModelSerializer):
956
992
  queryset = Favorite.objects.filter(
957
993
  content_type=content_type,
958
994
  object_id=data["content_object"].id,
995
+ user=self.context["request"].user,
959
996
  )
960
997
  for key in extras_lookup_content_type.keys():
961
998
  queryset = queryset.filter(
@@ -971,13 +1008,17 @@ class FavoriteSerializer(serializers.ModelSerializer):
971
1008
 
972
1009
  def validate(self, attrs):
973
1010
  validated_data = super().validate(attrs)
1011
+ name = validated_data["name"]
974
1012
  object_type = validated_data["object_type"]
975
- if object_type not in store_settings.FAVORITE_TYPES:
1013
+ if object_type not in store_settings.FAVORITE_TYPES and hasattr(
1014
+ sys.modules[__name__], object_type
1015
+ ):
976
1016
  raise serializers.ValidationError(
977
1017
  _(f"Cannot resolve keyword '{object_type}' into an object type")
978
1018
  )
1019
+ object_type_model = getattr(sys.modules[__name__], object_type)
979
1020
  object_instance = get_object_or_404(
980
- eval(object_type), pk=validated_data["object_id"]
1021
+ object_type_model, pk=validated_data["object_id"]
981
1022
  )
982
1023
  extras_data = {}
983
1024
  extras = []
@@ -993,24 +1034,59 @@ class FavoriteSerializer(serializers.ModelSerializer):
993
1034
  extras_data[extra_object_type].append(extra["object_id"])
994
1035
  try:
995
1036
  for key in extras_data.keys():
996
- model_class = eval(key)
997
- extras.append(model_class.objects.filter(id__in=extras_data[key]))
1037
+ model_class = getattr(sys.modules[__name__], key, None)
1038
+ if model_class:
1039
+ extras.append(model_class.objects.filter(id__in=extras_data[key]))
998
1040
  except ObjectDoesNotExist:
999
1041
  raise serializers.ValidationError(_("No matches the given query."))
1000
- except NameError as e:
1001
- raise NameError(e)
1002
1042
 
1003
1043
  validated_data = {
1004
1044
  "content_object": object_instance,
1005
1045
  "extras": extras,
1046
+ "name": name,
1006
1047
  }
1007
1048
  self._lookup_validation(validated_data)
1008
1049
  return validated_data
1009
1050
 
1010
1051
  def create(self, validated_data):
1011
- favorite = Favorite.add_favorite(
1012
- validated_data["content_object"],
1013
- self.context["request"].user,
1014
- validated_data["extras"],
1015
- )
1052
+ try:
1053
+ favorite = Favorite.add_favorite(
1054
+ content_object=validated_data["content_object"],
1055
+ user=self.context["request"].user,
1056
+ name=validated_data["name"],
1057
+ extras=validated_data["extras"],
1058
+ )
1059
+ except ValidationError as e:
1060
+ raise serializers.ValidationError(detail=e.message_dict)
1016
1061
  return favorite
1062
+
1063
+
1064
+ class WalletSerializer(serializers.ModelSerializer):
1065
+ class Meta:
1066
+ model = Wallet
1067
+ fields = ["id", "user", "balance", "name", "image", "image_thumbnail_medium"]
1068
+ extra_kwargs = {
1069
+ "user": {"read_only": True},
1070
+ "image_thumbnail_medium": {"read_only": True},
1071
+ }
1072
+
1073
+
1074
+ class WalletTopUpSerializer(serializers.Serializer):
1075
+ amount = serializers.DecimalField(
1076
+ max_digits=10, decimal_places=2, min_value=1, required=True
1077
+ )
1078
+ payment_method = serializers.PrimaryKeyRelatedField(
1079
+ queryset=PaymentMethod.objects.filter(
1080
+ payment_provider__in=[
1081
+ store_settings.TAP_CREDIT_CARD,
1082
+ store_settings.TAP_KNET,
1083
+ store_settings.TAP_ALL,
1084
+ ]
1085
+ ),
1086
+ required=True,
1087
+ )
1088
+
1089
+ def top_up_wallet(self, wallet):
1090
+ amount = self.validated_data["amount"]
1091
+ payment_method = self.validated_data["payment_method"]
1092
+ return wallet.top_up_wallet(amount=amount, payment_method=payment_method)
@@ -16,6 +16,7 @@ from ob_dj_store.apis.stores.views import (
16
16
  TaxViewSet,
17
17
  TransactionsViewSet,
18
18
  VariantView,
19
+ WalletViewSet,
19
20
  )
20
21
 
21
22
  app_name = "stores"
@@ -28,9 +29,6 @@ stores_router.register(r"order", OrderView, basename="order")
28
29
  stores_router.register(r"product", ProductView, basename="product")
29
30
  stores_router.register(r"variant", VariantView, basename="variant")
30
31
  stores_router.register(r"inventory", InventoryView, basename="inventory")
31
- stores_router.register(
32
- r"payment-method", PaymentMethodViewSet, basename="payment-method"
33
- )
34
32
  stores_router.register(
35
33
  r"shipping-method", ShippingMethodViewSet, basename="shipping-method"
36
34
  )
@@ -40,6 +38,8 @@ router.register(r"transaction", TransactionsViewSet, basename="transaction")
40
38
  router.register(r"cart", CartView, basename="cart")
41
39
  router.register(r"cart-item", CartItemView, basename="cart-item")
42
40
  router.register(r"favorite", FavoriteViewSet, basename="favorite")
41
+ router.register(r"wallet", WalletViewSet, basename="wallet")
42
+ router.register(r"payment-method", PaymentMethodViewSet, basename="payment-method")
43
43
  urlpatterns = [
44
44
  path(r"", include(router.urls)),
45
45
  path(r"", include(stores_router.urls)),