ob-dj-store 0.0.19__py3-none-any.whl → 0.0.23.2__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.
- ob_dj_store/apis/stores/filters.py +42 -19
- ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
- ob_dj_store/apis/stores/urls.py +6 -0
- ob_dj_store/apis/stores/views.py +140 -227
- ob_dj_store/apis/stripe/__init__.py +0 -0
- ob_dj_store/apis/stripe/serializers.py +185 -0
- ob_dj_store/apis/stripe/urls.py +25 -0
- ob_dj_store/apis/stripe/views.py +191 -0
- ob_dj_store/apis/tap/views.py +2 -6
- ob_dj_store/core/stores/admin.py +41 -38
- ob_dj_store/core/stores/admin_inlines.py +8 -13
- ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
- ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
- ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
- ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
- ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
- ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
- ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
- ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
- ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
- ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
- ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
- ob_dj_store/core/stores/gateway/tap/models.py +4 -13
- ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
- ob_dj_store/core/stores/managers.py +12 -4
- ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
- ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
- ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
- ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
- ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
- ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
- ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
- ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
- ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
- ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
- ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
- ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
- ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
- ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
- ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
- ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
- ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
- ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
- ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
- ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
- ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
- ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
- ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
- ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
- ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
- ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
- ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
- ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
- ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
- ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
- ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
- ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
- ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
- ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
- ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
- ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
- ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
- ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
- ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
- ob_dj_store/core/stores/models/__init__.py +9 -1
- ob_dj_store/core/stores/models/_address.py +1 -3
- ob_dj_store/core/stores/models/_cart.py +11 -5
- ob_dj_store/core/stores/models/_feedback.py +1 -3
- ob_dj_store/core/stores/models/_inventory.py +3 -2
- ob_dj_store/core/stores/models/_order.py +69 -20
- ob_dj_store/core/stores/models/_payment.py +28 -24
- ob_dj_store/core/stores/models/_product.py +31 -17
- ob_dj_store/core/stores/models/_store.py +9 -13
- ob_dj_store/core/stores/models/_wallet.py +34 -26
- ob_dj_store/core/stores/receivers.py +43 -27
- ob_dj_store/core/stores/utils.py +1 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
- {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import calendar
|
|
2
2
|
import logging
|
|
3
3
|
import typing
|
|
4
|
-
from
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
5
6
|
from decimal import Decimal
|
|
6
7
|
|
|
7
8
|
import pycountry
|
|
@@ -51,12 +52,14 @@ from ob_dj_store.core.stores.models import (
|
|
|
51
52
|
ShippingMethod,
|
|
52
53
|
Store,
|
|
53
54
|
Tax,
|
|
55
|
+
Tip,
|
|
56
|
+
TipAmount,
|
|
54
57
|
Wallet,
|
|
55
58
|
WalletMedia,
|
|
56
59
|
WalletTransaction,
|
|
57
60
|
)
|
|
58
61
|
from ob_dj_store.core.stores.models._inventory import Inventory
|
|
59
|
-
from ob_dj_store.core.stores.utils import PartnerAuth, distance
|
|
62
|
+
from ob_dj_store.core.stores.utils import PartnerAuth, distance, get_currency_by_country
|
|
60
63
|
|
|
61
64
|
logger = logging.getLogger(__name__)
|
|
62
65
|
|
|
@@ -283,6 +286,8 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
283
286
|
"created_at",
|
|
284
287
|
"updated_at",
|
|
285
288
|
"store_name",
|
|
289
|
+
"tip_percentage",
|
|
290
|
+
"tip_value",
|
|
286
291
|
)
|
|
287
292
|
extra_kwargs = {
|
|
288
293
|
"customer": {"read_only": True},
|
|
@@ -329,8 +334,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
329
334
|
return store
|
|
330
335
|
|
|
331
336
|
def _validate_user_address(
|
|
332
|
-
self,
|
|
333
|
-
attrs,
|
|
337
|
+
self, attrs,
|
|
334
338
|
):
|
|
335
339
|
if "shipping_address" not in attrs:
|
|
336
340
|
raise ValidationError(
|
|
@@ -362,7 +366,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
362
366
|
raise serializers.ValidationError(errors)
|
|
363
367
|
email = gift_details.get("email")
|
|
364
368
|
phone_number = gift_details.get("phone_number")
|
|
365
|
-
|
|
369
|
+
user = self.context["request"].user
|
|
366
370
|
if email and phone_number:
|
|
367
371
|
raise serializers.ValidationError(
|
|
368
372
|
_("Both Email and Phone number cannot be provided.")
|
|
@@ -376,7 +380,12 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
376
380
|
validate_email(email)
|
|
377
381
|
except ValidationError:
|
|
378
382
|
raise serializers.ValidationError(_("Invalid Email format."))
|
|
383
|
+
try:
|
|
384
|
+
if not gift_details["currency"]:
|
|
385
|
+
gift_details["currency"] = get_currency_by_country(user.country.code)
|
|
379
386
|
|
|
387
|
+
except Exception:
|
|
388
|
+
raise serializers.ValidationError("Currency is required")
|
|
380
389
|
if not pycountry.currencies.get(alpha_3=gift_details["currency"]):
|
|
381
390
|
raise serializers.ValidationError("Gift currency is not valid")
|
|
382
391
|
try:
|
|
@@ -396,29 +405,39 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
396
405
|
|
|
397
406
|
def _validate_pickup_time(self, store, pickup_time):
|
|
398
407
|
# validate that the pickup_time is always in the future
|
|
399
|
-
if pickup_time < localtime(now()):
|
|
408
|
+
if pickup_time < localtime(now(), self._get_store().timezone):
|
|
400
409
|
raise serializers.ValidationError(_("Pickup time must be in the future"))
|
|
401
410
|
pickup_time_time = pickup_time.time()
|
|
402
411
|
# validate that the pickup_time is between store's opening hours and closing hours
|
|
403
412
|
try:
|
|
404
413
|
op_hour = store.opening_hours.get(weekday=pickup_time.weekday() + 1)
|
|
414
|
+
from_hour = op_hour.from_hour
|
|
415
|
+
to_hour = op_hour.to_hour
|
|
405
416
|
if store.currency == "AED":
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
raise serializers.ValidationError(
|
|
412
|
-
_("Pickup time must be between store's opening hours")
|
|
417
|
+
try:
|
|
418
|
+
pickup_time_time += timedelta(hours=1)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.info(
|
|
421
|
+
f"The fix for UAE stores timezone failed due to this error {e}"
|
|
413
422
|
)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
423
|
+
|
|
424
|
+
if (
|
|
425
|
+
op_hour.is_open_after_midnight
|
|
426
|
+
and not op_hour.always_open
|
|
427
|
+
and (pickup_time_time > to_hour and pickup_time_time < from_hour)
|
|
418
428
|
):
|
|
419
429
|
raise serializers.ValidationError(
|
|
420
430
|
_("Pickup time must be between store's opening hours")
|
|
421
431
|
)
|
|
432
|
+
if (
|
|
433
|
+
not op_hour.is_open_after_midnight
|
|
434
|
+
and not op_hour.always_open
|
|
435
|
+
and not from_hour <= pickup_time_time <= to_hour
|
|
436
|
+
):
|
|
437
|
+
raise serializers.ValidationError(
|
|
438
|
+
_("Pickup time must be between store's opening hours")
|
|
439
|
+
)
|
|
440
|
+
|
|
422
441
|
except ObjectDoesNotExist:
|
|
423
442
|
logger.error(f"{store.name} Store doesn't have opening hours for ")
|
|
424
443
|
|
|
@@ -434,6 +453,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
434
453
|
attrs["extra_infos"] = {}
|
|
435
454
|
stores = Store.objects.filter(store_items__cart=user.cart).distinct()
|
|
436
455
|
gift_details = attrs["extra_infos"].get("gift_details", None)
|
|
456
|
+
unavailable_items = []
|
|
437
457
|
if gift_details:
|
|
438
458
|
self._validate_digital_product(gift_details, attrs)
|
|
439
459
|
currency = gift_details["currency"]
|
|
@@ -445,7 +465,17 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
445
465
|
if len(stores) > 1:
|
|
446
466
|
raise ValidationError(_("You cannot order from different stores"))
|
|
447
467
|
for item in user.cart.items.all():
|
|
448
|
-
if
|
|
468
|
+
if item.inventory:
|
|
469
|
+
if item.inventory.is_snoozed:
|
|
470
|
+
unavailable_items.append(item)
|
|
471
|
+
elif not item.inventory.is_uncountable:
|
|
472
|
+
if (
|
|
473
|
+
not item.inventory.quantity
|
|
474
|
+
or item.inventory.quantity == 0
|
|
475
|
+
):
|
|
476
|
+
unavailable_items.append(item)
|
|
477
|
+
|
|
478
|
+
elif not item.inventory:
|
|
449
479
|
if language and item.product_variant.product.name_arabic:
|
|
450
480
|
product_name = item.product_variant.product.name_arabic
|
|
451
481
|
else:
|
|
@@ -455,8 +485,14 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
455
485
|
message = (
|
|
456
486
|
f"{product_name} {variant_name} doesn't exist on this store"
|
|
457
487
|
)
|
|
488
|
+
unavailable_items.append(item)
|
|
458
489
|
raise ValidationError(_(message))
|
|
459
|
-
|
|
490
|
+
if unavailable_items:
|
|
491
|
+
raise ValidationError(
|
|
492
|
+
_(
|
|
493
|
+
"The cart has items that are not available in the selected store"
|
|
494
|
+
)
|
|
495
|
+
)
|
|
460
496
|
if "shipping_method" in attrs:
|
|
461
497
|
if (
|
|
462
498
|
attrs["shipping_method"].type
|
|
@@ -496,9 +532,15 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
496
532
|
return super().validate(attrs)
|
|
497
533
|
|
|
498
534
|
def perform_payment(self, amount, payment_method, order_store, orders, currency):
|
|
535
|
+
from django.conf import settings
|
|
536
|
+
|
|
537
|
+
from ob_dj_store.core.stores.gateway.stripe.utils import StripeException
|
|
499
538
|
from ob_dj_store.core.stores.gateway.tap.utils import TapException
|
|
500
539
|
|
|
501
540
|
user = self.context["request"].user
|
|
541
|
+
payment_transaction = None
|
|
542
|
+
charge_id = None
|
|
543
|
+
|
|
502
544
|
try:
|
|
503
545
|
payment = Payment.objects.create(
|
|
504
546
|
user=user,
|
|
@@ -507,21 +549,40 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
507
549
|
currency=currency,
|
|
508
550
|
orders=orders,
|
|
509
551
|
)
|
|
510
|
-
|
|
552
|
+
|
|
553
|
+
# Handle different payment gateways
|
|
554
|
+
if payment_method and payment_method.payment_provider == settings.STRIPE:
|
|
555
|
+
payment_transaction = payment.stripe_payment
|
|
556
|
+
charge_id = (
|
|
557
|
+
payment_transaction.payment_intent_id
|
|
558
|
+
if payment_transaction
|
|
559
|
+
else None
|
|
560
|
+
)
|
|
561
|
+
elif payment_method and payment_method.payment_provider in [
|
|
562
|
+
settings.TAP_CREDIT_CARD,
|
|
563
|
+
settings.TAP_KNET,
|
|
564
|
+
settings.TAP_ALL,
|
|
565
|
+
settings.MADA,
|
|
566
|
+
settings.BENEFIT,
|
|
567
|
+
]:
|
|
568
|
+
payment_transaction = payment.tap_payment
|
|
569
|
+
charge_id = (
|
|
570
|
+
payment_transaction.charge_id if payment_transaction else None
|
|
571
|
+
)
|
|
572
|
+
|
|
511
573
|
except ValidationError as err:
|
|
512
574
|
raise serializers.ValidationError(detail=err.messages)
|
|
513
|
-
except TapException as err:
|
|
514
|
-
raise serializers.ValidationError({"
|
|
575
|
+
except (TapException, StripeException) as err:
|
|
576
|
+
raise serializers.ValidationError({"payment_gateway": _(str(err))})
|
|
515
577
|
except ObjectDoesNotExist as err:
|
|
516
578
|
logger.info(
|
|
517
579
|
f"Payment Object not created: user:{user}, method:{payment_method}, currency:{currency}, error:{err}"
|
|
518
580
|
)
|
|
519
|
-
tap_transaction = None
|
|
520
581
|
|
|
521
582
|
return {
|
|
522
583
|
"orders": orders,
|
|
523
584
|
"payment_url": payment.payment_url,
|
|
524
|
-
"charge_id":
|
|
585
|
+
"charge_id": charge_id,
|
|
525
586
|
}
|
|
526
587
|
|
|
527
588
|
def create(self, validated_data: typing.Dict):
|
|
@@ -533,7 +594,12 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
533
594
|
amount = gift_details["price"]
|
|
534
595
|
order = Order.objects.create(customer=user, **validated_data)
|
|
535
596
|
orders.append(order)
|
|
536
|
-
|
|
597
|
+
try:
|
|
598
|
+
default_currency = get_currency_by_country(user.country.code)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
default_currency = "KWD"
|
|
601
|
+
logger.info(f"Couldn't fetch currency due to this error {e}")
|
|
602
|
+
currency = gift_details.get("currency", default_currency)
|
|
537
603
|
else:
|
|
538
604
|
cart = user.cart
|
|
539
605
|
stores = Store.objects.filter(store_items__cart=cart).distinct()
|
|
@@ -552,7 +618,11 @@ class OrderSerializer(serializers.ModelSerializer):
|
|
|
552
618
|
attributes=list(item.attribute_choices.all()),
|
|
553
619
|
notes=item.notes,
|
|
554
620
|
)
|
|
621
|
+
if order.tip_percentage:
|
|
622
|
+
order.tip_value = order.calculate_tip()
|
|
623
|
+
order.save()
|
|
555
624
|
orders.append(order)
|
|
625
|
+
|
|
556
626
|
amount = Decimal(
|
|
557
627
|
sum(map(lambda order: Decimal(order.total_amount) or 0, orders))
|
|
558
628
|
)
|
|
@@ -703,9 +773,7 @@ class CartItemSerializer(
|
|
|
703
773
|
"is_multi_variant",
|
|
704
774
|
)
|
|
705
775
|
extra_kwargs = {
|
|
706
|
-
"store": {
|
|
707
|
-
"required": True,
|
|
708
|
-
},
|
|
776
|
+
"store": {"required": True,},
|
|
709
777
|
}
|
|
710
778
|
|
|
711
779
|
def get_is_multi_variant(self, obj):
|
|
@@ -713,7 +781,10 @@ class CartItemSerializer(
|
|
|
713
781
|
return product.product_variants.all().count() > 1
|
|
714
782
|
|
|
715
783
|
def get_is_available_in_store(self, obj):
|
|
716
|
-
|
|
784
|
+
if obj.inventory:
|
|
785
|
+
if obj.inventory.quantity:
|
|
786
|
+
return True
|
|
787
|
+
return False
|
|
717
788
|
|
|
718
789
|
def get_product_id(self, obj):
|
|
719
790
|
return obj.product_variant.product.id
|
|
@@ -743,9 +814,9 @@ class CartItemSerializer(
|
|
|
743
814
|
favorites = Favorite.objects.favorites_for_object(
|
|
744
815
|
obj.product_variant.product, user
|
|
745
816
|
)
|
|
746
|
-
customization = [
|
|
747
|
-
obj.
|
|
748
|
-
]
|
|
817
|
+
customization = [obj.product_variant,] + [
|
|
818
|
+
attribute_choice for attribute_choice in obj.attribute_choices.all()
|
|
819
|
+
]
|
|
749
820
|
for favorite in favorites:
|
|
750
821
|
content_objects = [
|
|
751
822
|
instance.content_object for instance in favorite.extras.all()
|
|
@@ -820,10 +891,19 @@ class CartSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
820
891
|
# update or create instance items
|
|
821
892
|
for item in validated_data["items"]:
|
|
822
893
|
attribute_choices = item.pop("attribute_choices", None)
|
|
823
|
-
|
|
894
|
+
logger.info("cart item :", item)
|
|
895
|
+
cart_item, created = CartItem.objects.get_or_create(
|
|
824
896
|
cart=instance,
|
|
825
|
-
|
|
897
|
+
pk=item.pop("id", None),
|
|
898
|
+
defaults={
|
|
899
|
+
"cart": instance,
|
|
900
|
+
"product_variant": item.pop("product_variant", None),
|
|
901
|
+
"store": item.pop("store", None),
|
|
902
|
+
"notes": item.pop("notes", None),
|
|
903
|
+
"quantity": item.pop("quantity", None),
|
|
904
|
+
},
|
|
826
905
|
)
|
|
906
|
+
|
|
827
907
|
if attribute_choices:
|
|
828
908
|
cart_item.attribute_choices.set(attribute_choices)
|
|
829
909
|
cart_item.save()
|
|
@@ -899,6 +979,44 @@ class ProductSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeria
|
|
|
899
979
|
return data
|
|
900
980
|
|
|
901
981
|
|
|
982
|
+
class ProductSearchSerializer(
|
|
983
|
+
ArabicFieldsMixin, FavoriteMixin, serializers.ModelSerializer
|
|
984
|
+
):
|
|
985
|
+
is_snoozed = serializers.SerializerMethodField()
|
|
986
|
+
is_available = serializers.SerializerMethodField()
|
|
987
|
+
|
|
988
|
+
class Meta:
|
|
989
|
+
model = Product
|
|
990
|
+
fields = (
|
|
991
|
+
"id",
|
|
992
|
+
"name",
|
|
993
|
+
"name_arabic",
|
|
994
|
+
"slug",
|
|
995
|
+
"description",
|
|
996
|
+
"description_arabic",
|
|
997
|
+
"is_snoozed",
|
|
998
|
+
"is_available",
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
def get_store_id(self):
|
|
1002
|
+
return self.context["view"].kwargs["store_pk"]
|
|
1003
|
+
|
|
1004
|
+
def get_inventory_for_store(self, product, store_id):
|
|
1005
|
+
if store_id:
|
|
1006
|
+
return product.get_inventory(store_id)
|
|
1007
|
+
return None
|
|
1008
|
+
|
|
1009
|
+
def get_is_snoozed(self, obj):
|
|
1010
|
+
store_id = self.get_store_id()
|
|
1011
|
+
inventory = self.get_inventory_for_store(obj, store_id)
|
|
1012
|
+
return obj.is_snoozed(store_id=store_id) if inventory else False
|
|
1013
|
+
|
|
1014
|
+
def get_is_available(self, obj):
|
|
1015
|
+
store_id = self.get_store_id()
|
|
1016
|
+
inventory = self.get_inventory_for_store(obj, store_id)
|
|
1017
|
+
return bool(inventory and (inventory.quantity or inventory.is_uncountable))
|
|
1018
|
+
|
|
1019
|
+
|
|
902
1020
|
class ProductListSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
903
1021
|
product_images = ProductMediaSerializer(many=True, source="images")
|
|
904
1022
|
|
|
@@ -948,11 +1066,13 @@ class SubCategorySerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
948
1066
|
def get_is_available(self, obj) -> bool:
|
|
949
1067
|
store_id = self.context["request"].query_params.get("store", None)
|
|
950
1068
|
if store_id:
|
|
1069
|
+
local_tz = Store.objects.get(id=store_id).timezone
|
|
1070
|
+
current_time = localtime(now(), local_tz) if local_tz else localtime(now())
|
|
951
1071
|
for availability_hours in obj.parent.availability_hours.all():
|
|
952
1072
|
if availability_hours.category == obj.parent:
|
|
953
1073
|
return (
|
|
954
1074
|
availability_hours.from_hour
|
|
955
|
-
<=
|
|
1075
|
+
<= current_time.time()
|
|
956
1076
|
<= availability_hours.to_hour
|
|
957
1077
|
)
|
|
958
1078
|
return False
|
|
@@ -984,11 +1104,13 @@ class CategorySerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
984
1104
|
def get_is_available(self, obj) -> bool:
|
|
985
1105
|
store_id = self.context["request"].query_params.get("store", None)
|
|
986
1106
|
if store_id:
|
|
1107
|
+
local_tz = Store.objects.get(id=store_id).timezone
|
|
1108
|
+
current_time = localtime(now(), local_tz) if local_tz else localtime(now())
|
|
987
1109
|
for availability_hours in obj.availability_hours.all():
|
|
988
1110
|
if availability_hours.category == obj:
|
|
989
1111
|
return (
|
|
990
1112
|
availability_hours.from_hour
|
|
991
|
-
<=
|
|
1113
|
+
<= current_time.time()
|
|
992
1114
|
<= availability_hours.to_hour
|
|
993
1115
|
)
|
|
994
1116
|
return False
|
|
@@ -1099,6 +1221,7 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
|
|
|
1099
1221
|
shipping_methods = ShippingMethodSerializer(many=True, read_only=True)
|
|
1100
1222
|
distance = serializers.SerializerMethodField()
|
|
1101
1223
|
current_day_opening_hours = serializers.SerializerMethodField()
|
|
1224
|
+
timezone = serializers.SerializerMethodField()
|
|
1102
1225
|
|
|
1103
1226
|
class Meta:
|
|
1104
1227
|
model = Store
|
|
@@ -1127,7 +1250,7 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
|
|
|
1127
1250
|
"image",
|
|
1128
1251
|
"busy_mode",
|
|
1129
1252
|
"name_arabic",
|
|
1130
|
-
"
|
|
1253
|
+
"timezone",
|
|
1131
1254
|
)
|
|
1132
1255
|
extra_kwargs = {
|
|
1133
1256
|
"image": {"read_only": True, "required": False},
|
|
@@ -1136,26 +1259,26 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
|
|
|
1136
1259
|
def get_is_closed(self, obj):
|
|
1137
1260
|
if obj.busy_mode:
|
|
1138
1261
|
return True
|
|
1139
|
-
|
|
1140
|
-
current_time = localtime(now())
|
|
1262
|
+
current_time = localtime(now(), obj.timezone)
|
|
1141
1263
|
current_op_hour = obj.current_opening_hours
|
|
1142
1264
|
if current_op_hour:
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1265
|
+
from_hour = current_op_hour.from_hour
|
|
1266
|
+
to_hour = current_op_hour.to_hour
|
|
1267
|
+
if current_time.tzinfo != "Asia/Dubai":
|
|
1268
|
+
if obj.currency == "AED":
|
|
1269
|
+
try:
|
|
1270
|
+
current_time += timedelta(hours=1)
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
logger.info(
|
|
1273
|
+
f"The fix for UAE stores timezone failed due to this error {e}"
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
if current_op_hour.is_open_after_midnight:
|
|
1277
|
+
return True if to_hour < current_time.time() < from_hour else False
|
|
1151
1278
|
|
|
1152
1279
|
if current_op_hour.always_open:
|
|
1153
1280
|
return False
|
|
1154
|
-
return (
|
|
1155
|
-
not current_op_hour.from_hour
|
|
1156
|
-
<= current_time.time()
|
|
1157
|
-
<= current_op_hour.to_hour
|
|
1158
|
-
)
|
|
1281
|
+
return not from_hour <= current_time.time() <= to_hour
|
|
1159
1282
|
return True
|
|
1160
1283
|
|
|
1161
1284
|
def get_in_range_delivery(self, obj):
|
|
@@ -1255,6 +1378,9 @@ class StoreSerializer(ArabicFieldsMixin, FavoriteMixin, serializers.ModelSeriali
|
|
|
1255
1378
|
op_hour = f"{settings.DEFAULT_OPENING_HOURS[0]['from_hour']} - {settings.DEFAULT_OPENING_HOURS[0]['to_hour']}"
|
|
1256
1379
|
return op_hour
|
|
1257
1380
|
|
|
1381
|
+
def get_timezone(self, obj):
|
|
1382
|
+
return str(obj.timezone)
|
|
1383
|
+
|
|
1258
1384
|
def to_representation(self, instance):
|
|
1259
1385
|
return super().to_representation(instance)
|
|
1260
1386
|
|
|
@@ -1293,10 +1419,15 @@ class TaxSerializer(serializers.ModelSerializer):
|
|
|
1293
1419
|
|
|
1294
1420
|
|
|
1295
1421
|
class StoreListSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
1422
|
+
timezone = serializers.SerializerMethodField()
|
|
1423
|
+
|
|
1296
1424
|
class Meta:
|
|
1297
1425
|
model = Store
|
|
1298
1426
|
fields = "__all__"
|
|
1299
1427
|
|
|
1428
|
+
def get_timezone(self, obj):
|
|
1429
|
+
return str(obj.timezone)
|
|
1430
|
+
|
|
1300
1431
|
|
|
1301
1432
|
class GenericSerializer(serializers.Serializer):
|
|
1302
1433
|
"""
|
|
@@ -1392,9 +1523,12 @@ class FavoriteSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
1392
1523
|
try:
|
|
1393
1524
|
extra = obj.extras.get(content_type=content_type)
|
|
1394
1525
|
inventory = extra.content_object.inventories.get(store=store_id)
|
|
1395
|
-
if inventory.
|
|
1396
|
-
return
|
|
1397
|
-
|
|
1526
|
+
if inventory.is_snoozed:
|
|
1527
|
+
return False
|
|
1528
|
+
else:
|
|
1529
|
+
if inventory.is_uncountable:
|
|
1530
|
+
return True
|
|
1531
|
+
return False
|
|
1398
1532
|
except ObjectDoesNotExist:
|
|
1399
1533
|
return False
|
|
1400
1534
|
return None
|
|
@@ -1566,6 +1700,7 @@ class WalletTopUpSerializer(serializers.Serializer):
|
|
|
1566
1700
|
store_settings.GOOGLE_PAY,
|
|
1567
1701
|
store_settings.MADA,
|
|
1568
1702
|
store_settings.BENEFIT,
|
|
1703
|
+
store_settings.STRIPE,
|
|
1569
1704
|
]
|
|
1570
1705
|
),
|
|
1571
1706
|
required=True,
|
|
@@ -1597,6 +1732,39 @@ class WalletTransactionSerializer(serializers.ModelSerializer):
|
|
|
1597
1732
|
]
|
|
1598
1733
|
|
|
1599
1734
|
|
|
1735
|
+
class GroupedWalletTransactionListSerializer(serializers.ListSerializer):
|
|
1736
|
+
def to_representation(self, data):
|
|
1737
|
+
result_dict = OrderedDict()
|
|
1738
|
+
for wallettransaction in data:
|
|
1739
|
+
year = wallettransaction.created_at.year
|
|
1740
|
+
month = wallettransaction.created_at.month
|
|
1741
|
+
title = f"{datetime(year, month, 1).strftime('%b')}, {year}"
|
|
1742
|
+
|
|
1743
|
+
if title not in result_dict:
|
|
1744
|
+
result_dict[title] = {"title": title, "data": []}
|
|
1745
|
+
|
|
1746
|
+
result_dict[title]["data"].append(
|
|
1747
|
+
self.child.to_representation(wallettransaction)
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
return list(result_dict.values())
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
class WalletTransactionListSerializer(serializers.ModelSerializer):
|
|
1754
|
+
class Meta:
|
|
1755
|
+
model = WalletTransaction
|
|
1756
|
+
fields = [
|
|
1757
|
+
"id",
|
|
1758
|
+
"type",
|
|
1759
|
+
"amount",
|
|
1760
|
+
"created_at",
|
|
1761
|
+
"wallet",
|
|
1762
|
+
"is_cashback",
|
|
1763
|
+
"is_refund",
|
|
1764
|
+
]
|
|
1765
|
+
list_serializer_class = GroupedWalletTransactionListSerializer
|
|
1766
|
+
|
|
1767
|
+
|
|
1600
1768
|
class ReorderSerializer(serializers.Serializer):
|
|
1601
1769
|
force_cart = serializers.BooleanField(default=False)
|
|
1602
1770
|
|
|
@@ -1769,9 +1937,7 @@ class PartnerAuthInfoSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
1769
1937
|
status=OneTruePairing.Statuses.init,
|
|
1770
1938
|
created_at__gte=timeout,
|
|
1771
1939
|
).get(
|
|
1772
|
-
email=attrs["email"],
|
|
1773
|
-
partner_otp_auth__partner=partner,
|
|
1774
|
-
user=user,
|
|
1940
|
+
email=attrs["email"], partner_otp_auth__partner=partner, user=user,
|
|
1775
1941
|
)
|
|
1776
1942
|
self.context["otp"] = otp
|
|
1777
1943
|
except ObjectDoesNotExist as e:
|
|
@@ -1808,11 +1974,7 @@ class PartnerAuthInfoSerializer(ArabicFieldsMixin, serializers.ModelSerializer):
|
|
|
1808
1974
|
otp.status = OneTruePairing.Statuses.used
|
|
1809
1975
|
otp.save()
|
|
1810
1976
|
partner_auth_info = PartnerAuthInfo.objects.update_or_create(
|
|
1811
|
-
user=user,
|
|
1812
|
-
defaults={
|
|
1813
|
-
"partner": partner,
|
|
1814
|
-
"email": validated_data["email"],
|
|
1815
|
-
},
|
|
1977
|
+
user=user, defaults={"partner": partner, "email": validated_data["email"],},
|
|
1816
1978
|
)
|
|
1817
1979
|
return partner_auth_info
|
|
1818
1980
|
|
|
@@ -1836,3 +1998,34 @@ class CountryPaymentMethodSerialzier(serializers.ModelSerializer):
|
|
|
1836
1998
|
instance=qs, context=self.context, many=True
|
|
1837
1999
|
).data
|
|
1838
2000
|
return data
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
class TipSerializer(serializers.ModelSerializer):
|
|
2004
|
+
tip_amounts = serializers.SerializerMethodField()
|
|
2005
|
+
|
|
2006
|
+
class Meta:
|
|
2007
|
+
model = Tip
|
|
2008
|
+
fields = (
|
|
2009
|
+
"id",
|
|
2010
|
+
"name",
|
|
2011
|
+
"description",
|
|
2012
|
+
"is_applied",
|
|
2013
|
+
"country",
|
|
2014
|
+
"is_active",
|
|
2015
|
+
"tip_amounts",
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
def get_tip_amounts(self, obj):
|
|
2019
|
+
from .serializers import TipAmountSerializer # avoid circular import
|
|
2020
|
+
|
|
2021
|
+
tip_amounts = obj.tip_amounts.all()
|
|
2022
|
+
return TipAmountSerializer(tip_amounts, many=True).data
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
class TipAmountSerializer(serializers.ModelSerializer):
|
|
2026
|
+
class Meta:
|
|
2027
|
+
model = TipAmount
|
|
2028
|
+
fields = (
|
|
2029
|
+
"id",
|
|
2030
|
+
"percentage",
|
|
2031
|
+
)
|
ob_dj_store/apis/stores/urls.py
CHANGED
|
@@ -18,8 +18,10 @@ from ob_dj_store.apis.stores.views import (
|
|
|
18
18
|
ShippingMethodViewSet,
|
|
19
19
|
StoreView,
|
|
20
20
|
TaxViewSet,
|
|
21
|
+
TipsViewSet,
|
|
21
22
|
TransactionsViewSet,
|
|
22
23
|
VariantView,
|
|
24
|
+
WalletV2ViewSet,
|
|
23
25
|
WalletViewSet,
|
|
24
26
|
)
|
|
25
27
|
|
|
@@ -43,6 +45,7 @@ router.register(r"cart", CartView, basename="cart")
|
|
|
43
45
|
router.register(r"cart-item", CartItemView, basename="cart-item")
|
|
44
46
|
router.register(r"favorite", FavoriteViewSet, basename="favorite")
|
|
45
47
|
router.register(r"wallet", WalletViewSet, basename="wallet")
|
|
48
|
+
router.register(r"wallet_v2", WalletV2ViewSet, basename="wallet_v2")
|
|
46
49
|
router.register(r"payment-method", PaymentMethodViewSet, basename="payment-method")
|
|
47
50
|
router.register(r"order", ReorderViewSet, basename="re-order")
|
|
48
51
|
router.register(r"partner/auth", PartnerAuthInfoViewSet, basename="auth")
|
|
@@ -52,6 +55,9 @@ router.register(
|
|
|
52
55
|
CountryPaymentMethodsViewSet,
|
|
53
56
|
basename="country-payment-methods",
|
|
54
57
|
)
|
|
58
|
+
router.register(
|
|
59
|
+
r"tip", TipsViewSet, basename="tip",
|
|
60
|
+
)
|
|
55
61
|
|
|
56
62
|
urlpatterns = [
|
|
57
63
|
path(r"", include(router.urls)),
|