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.
Files changed (82) hide show
  1. ob_dj_store/apis/stores/filters.py +42 -19
  2. ob_dj_store/apis/stores/rest/serializers/serializers.py +256 -63
  3. ob_dj_store/apis/stores/urls.py +6 -0
  4. ob_dj_store/apis/stores/views.py +140 -227
  5. ob_dj_store/apis/stripe/__init__.py +0 -0
  6. ob_dj_store/apis/stripe/serializers.py +185 -0
  7. ob_dj_store/apis/stripe/urls.py +25 -0
  8. ob_dj_store/apis/stripe/views.py +191 -0
  9. ob_dj_store/apis/tap/views.py +2 -6
  10. ob_dj_store/core/stores/admin.py +41 -38
  11. ob_dj_store/core/stores/admin_inlines.py +8 -13
  12. ob_dj_store/core/stores/gateway/stripe/__init__.py +2 -0
  13. ob_dj_store/core/stores/gateway/stripe/admin.py +77 -0
  14. ob_dj_store/core/stores/gateway/stripe/apps.py +9 -0
  15. ob_dj_store/core/stores/gateway/stripe/managers.py +35 -0
  16. ob_dj_store/core/stores/gateway/stripe/migrations/0001_initial.py +168 -0
  17. ob_dj_store/core/stores/gateway/stripe/migrations/__init__.py +1 -0
  18. ob_dj_store/core/stores/gateway/stripe/models.py +174 -0
  19. ob_dj_store/core/stores/gateway/stripe/utils.py +170 -0
  20. ob_dj_store/core/stores/gateway/tap/admin.py +1 -3
  21. ob_dj_store/core/stores/gateway/tap/managers.py +1 -6
  22. ob_dj_store/core/stores/gateway/tap/migrations/0001_initial.py +1 -3
  23. ob_dj_store/core/stores/gateway/tap/migrations/0008_alter_tappayment_user.py +25 -0
  24. ob_dj_store/core/stores/gateway/tap/models.py +4 -13
  25. ob_dj_store/core/stores/gateway/tap/utils.py +2 -7
  26. ob_dj_store/core/stores/managers.py +12 -4
  27. ob_dj_store/core/stores/migrations/0001_initial.py +1 -4
  28. ob_dj_store/core/stores/migrations/0005_auto_20220425_2119.py +2 -5
  29. ob_dj_store/core/stores/migrations/0005_auto_20220427_1729.py +1 -2
  30. ob_dj_store/core/stores/migrations/0006_auto_20220428_0100.py +2 -8
  31. ob_dj_store/core/stores/migrations/0007_cart_cartitem_order_orderitem.py +2 -8
  32. ob_dj_store/core/stores/migrations/0010_auto_20220509_1633.py +1 -4
  33. ob_dj_store/core/stores/migrations/0012_auto_20220514_0633.py +1 -4
  34. ob_dj_store/core/stores/migrations/0013_auto_20220518_1539.py +1 -4
  35. ob_dj_store/core/stores/migrations/0014_auto_20220519_0018.py +3 -12
  36. ob_dj_store/core/stores/migrations/0017_auto_20220524_0912.py +3 -10
  37. ob_dj_store/core/stores/migrations/0018_auto_20220524_1613.py +1 -3
  38. ob_dj_store/core/stores/migrations/0021_auto_20220531_1849.py +1 -4
  39. ob_dj_store/core/stores/migrations/0026_auto_20220630_1913.py +8 -32
  40. ob_dj_store/core/stores/migrations/0031_auto_20220811_1733.py +1 -4
  41. ob_dj_store/core/stores/migrations/0033_auto_20220815_0133.py +2 -8
  42. ob_dj_store/core/stores/migrations/0039_auto_20220831_1521.py +1 -4
  43. ob_dj_store/core/stores/migrations/0044_remove_productvariant_has_inventory.py +1 -4
  44. ob_dj_store/core/stores/migrations/0049_auto_20221029_1524.py +2 -8
  45. ob_dj_store/core/stores/migrations/0050_favoriteextra.py +1 -3
  46. ob_dj_store/core/stores/migrations/0052_auto_20221129_1732.py +2 -8
  47. ob_dj_store/core/stores/migrations/0059_auto_20230217_2006.py +2 -8
  48. ob_dj_store/core/stores/migrations/0062_auto_20230226_2005.py +2 -6
  49. ob_dj_store/core/stores/migrations/0064_auto_20230228_1814.py +1 -2
  50. ob_dj_store/core/stores/migrations/0066_auto_20230304_1532.py +2 -8
  51. ob_dj_store/core/stores/migrations/0070_auto_20230323_1628.py +1 -4
  52. ob_dj_store/core/stores/migrations/0071_auto_20230328_1825.py +2 -5
  53. ob_dj_store/core/stores/migrations/0082_auto_20230613_1424.py +1 -4
  54. ob_dj_store/core/stores/migrations/0084_payment_result.py +1 -3
  55. ob_dj_store/core/stores/migrations/0087_auto_20230828_2138.py +1 -4
  56. ob_dj_store/core/stores/migrations/0097_auto_20231108_1939.py +1 -4
  57. ob_dj_store/core/stores/migrations/0100_remove_shippingmethod_type_arabic.py +1 -4
  58. ob_dj_store/core/stores/migrations/0106_alter_paymentmethod_payment_provider.py +35 -0
  59. ob_dj_store/core/stores/migrations/0107_auto_20250425_2059.py +29 -0
  60. ob_dj_store/core/stores/migrations/0108_alter_paymentmethod_payment_provider.py +35 -0
  61. ob_dj_store/core/stores/migrations/0109_wallettransaction_cashback_type.py +27 -0
  62. ob_dj_store/core/stores/migrations/0110_auto_20250923_1714.py +26 -0
  63. ob_dj_store/core/stores/migrations/0111_auto_20251023_1700.py +35 -0
  64. ob_dj_store/core/stores/migrations/0112_auto_20251027_1739.py +98 -0
  65. ob_dj_store/core/stores/migrations/0113_order_tax_value.py +20 -0
  66. ob_dj_store/core/stores/migrations/0114_store_mask_customer_info.py +18 -0
  67. ob_dj_store/core/stores/models/__init__.py +9 -1
  68. ob_dj_store/core/stores/models/_address.py +1 -3
  69. ob_dj_store/core/stores/models/_cart.py +11 -5
  70. ob_dj_store/core/stores/models/_feedback.py +1 -3
  71. ob_dj_store/core/stores/models/_inventory.py +3 -2
  72. ob_dj_store/core/stores/models/_order.py +69 -20
  73. ob_dj_store/core/stores/models/_payment.py +28 -24
  74. ob_dj_store/core/stores/models/_product.py +31 -17
  75. ob_dj_store/core/stores/models/_store.py +9 -13
  76. ob_dj_store/core/stores/models/_wallet.py +34 -26
  77. ob_dj_store/core/stores/receivers.py +43 -27
  78. ob_dj_store/core/stores/utils.py +1 -2
  79. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/METADATA +3 -2
  80. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/RECORD +82 -60
  81. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/WHEEL +1 -1
  82. {ob_dj_store-0.0.19.dist-info → ob_dj_store-0.0.23.2.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,185 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.utils.translation import gettext_lazy as _
3
+ from rest_framework import serializers
4
+
5
+ from ob_dj_store.core.stores.gateway.stripe.models import StripeCustomer, StripePayment
6
+ from ob_dj_store.core.stores.gateway.stripe.utils import StripeException
7
+ from ob_dj_store.core.stores.models import Payment, PaymentMethod
8
+
9
+ User = get_user_model()
10
+
11
+
12
+ class StripeCustomerSerializer(serializers.ModelSerializer):
13
+ """Serializer for Stripe customer information"""
14
+
15
+ class Meta:
16
+ model = StripeCustomer
17
+ fields = [
18
+ "stripe_customer_id",
19
+ "first_name",
20
+ "last_name",
21
+ "email",
22
+ "phone_number",
23
+ "created_at",
24
+ ]
25
+ read_only_fields = ["stripe_customer_id", "created_at"]
26
+
27
+
28
+ class StripePaymentSerializer(serializers.ModelSerializer):
29
+ """Serializer for Stripe payment information"""
30
+
31
+ amount = serializers.DecimalField(max_digits=10, decimal_places=3, read_only=True)
32
+ currency = serializers.CharField(read_only=True)
33
+ payment_url = serializers.CharField(read_only=True)
34
+
35
+ class Meta:
36
+ model = StripePayment
37
+ fields = [
38
+ "payment_intent_id",
39
+ "client_secret",
40
+ "status",
41
+ "source",
42
+ "amount",
43
+ "currency",
44
+ "payment_url",
45
+ "created_at",
46
+ "updated_at",
47
+ ]
48
+ read_only_fields = [
49
+ "payment_intent_id",
50
+ "client_secret",
51
+ "status",
52
+ "source",
53
+ "amount",
54
+ "currency",
55
+ "payment_url",
56
+ "created_at",
57
+ "updated_at",
58
+ ]
59
+
60
+
61
+ class CreateStripePaymentSerializer(serializers.Serializer):
62
+ """Serializer for creating Stripe payments"""
63
+
64
+ payment_id = serializers.IntegerField(
65
+ help_text="ID of the Payment object to process with Stripe"
66
+ )
67
+ return_url = serializers.URLField(
68
+ required=False,
69
+ help_text="URL to redirect to after payment completion (optional)",
70
+ )
71
+
72
+ def validate_payment_id(self, value):
73
+ """Validate that the payment exists and belongs to the user"""
74
+ user = self.context["request"].user
75
+
76
+ try:
77
+ payment = Payment.objects.get(id=value, user=user)
78
+ except Payment.DoesNotExist:
79
+ raise serializers.ValidationError(
80
+ _("Payment not found or does not belong to you.")
81
+ )
82
+
83
+ # Check if payment method is Stripe
84
+ if payment.method.payment_provider != "stripe":
85
+ raise serializers.ValidationError(_("Payment method must be Stripe."))
86
+
87
+ # Check if payment is already processed
88
+ if hasattr(payment, "stripe_payment"):
89
+ if payment.stripe_payment.status in ["succeeded", "canceled"]:
90
+ raise serializers.ValidationError(
91
+ _("Payment has already been processed.")
92
+ )
93
+
94
+ return value
95
+
96
+ def create(self, validated_data):
97
+ """Create a Stripe PaymentIntent for the payment"""
98
+ user = self.context["request"].user
99
+ payment_id = validated_data["payment_id"]
100
+ return_url = validated_data.get("return_url")
101
+
102
+ payment = Payment.objects.get(id=payment_id, user=user)
103
+
104
+ try:
105
+ # Create or get existing Stripe payment
106
+ if hasattr(payment, "stripe_payment"):
107
+ stripe_payment = payment.stripe_payment
108
+ else:
109
+ stripe_payment = StripePayment.objects.create(
110
+ payment=payment, user=user
111
+ )
112
+
113
+ return {
114
+ "payment_intent_id": stripe_payment.payment_intent_id,
115
+ "client_secret": stripe_payment.client_secret,
116
+ "status": stripe_payment.status,
117
+ "amount": stripe_payment.amount,
118
+ "currency": stripe_payment.currency,
119
+ "return_url": return_url,
120
+ }
121
+
122
+ except StripeException as e:
123
+ raise serializers.ValidationError({"stripe": str(e)})
124
+
125
+
126
+ class StripePaymentStatusSerializer(serializers.Serializer):
127
+ """Serializer for checking Stripe payment status"""
128
+
129
+ payment_intent_id = serializers.CharField(
130
+ help_text="Stripe PaymentIntent ID to check status for"
131
+ )
132
+
133
+ def validate_payment_intent_id(self, value):
134
+ """Validate that the payment intent exists and belongs to the user"""
135
+ user = self.context["request"].user
136
+
137
+ try:
138
+ stripe_payment = StripePayment.objects.get(
139
+ payment_intent_id=value, user=user
140
+ )
141
+ except StripePayment.DoesNotExist:
142
+ raise serializers.ValidationError(
143
+ _("Payment intent not found or does not belong to you.")
144
+ )
145
+
146
+ return value
147
+
148
+
149
+ class StripeWebhookEventSerializer(serializers.Serializer):
150
+ """Serializer for processing Stripe webhook events"""
151
+
152
+ id = serializers.CharField()
153
+ type = serializers.CharField()
154
+ data = serializers.DictField()
155
+
156
+ def validate(self, attrs):
157
+ """Validate webhook event structure"""
158
+ if "object" not in attrs.get("data", {}):
159
+ raise serializers.ValidationError(_("Invalid webhook event structure."))
160
+
161
+ return attrs
162
+
163
+
164
+ class PaymentMethodSerializer(serializers.ModelSerializer):
165
+ """Serializer for payment methods with Stripe support"""
166
+
167
+ supports_stripe = serializers.SerializerMethodField()
168
+
169
+ class Meta:
170
+ model = PaymentMethod
171
+ fields = [
172
+ "id",
173
+ "name",
174
+ "name_arabic",
175
+ "description",
176
+ "description_arabic",
177
+ "payment_provider",
178
+ "is_active",
179
+ "supports_stripe",
180
+ ]
181
+ read_only_fields = ["supports_stripe"]
182
+
183
+ def get_supports_stripe(self, obj):
184
+ """Check if this payment method supports Stripe"""
185
+ return obj.payment_provider == "stripe"
@@ -0,0 +1,25 @@
1
+ from django.conf.urls import include
2
+ from django.urls import path
3
+ from rest_framework.routers import SimpleRouter
4
+
5
+ from ob_dj_store.apis.stripe.views import StripePaymentViewSet, StripeWebhookViewSet
6
+
7
+ app_name = "stripe_gateway"
8
+
9
+ router = SimpleRouter(trailing_slash=False)
10
+
11
+ # Payment management endpoints
12
+ router.register(r"payments", StripePaymentViewSet, basename="stripe-payment")
13
+
14
+ # Webhook endpoints
15
+ router.register(r"webhook", StripeWebhookViewSet, basename="stripe-webhook")
16
+
17
+ urlpatterns = [
18
+ # Direct webhook URL for easier access from Stripe
19
+ path(
20
+ "callback/",
21
+ StripeWebhookViewSet.as_view({"post": "callback"}),
22
+ name="webhook-callback",
23
+ ),
24
+ path("", include(router.urls)),
25
+ ]
@@ -0,0 +1,191 @@
1
+ import logging
2
+
3
+ from django.http import HttpResponse, HttpResponseBadRequest
4
+ from django.utils.decorators import method_decorator
5
+ from django.views.decorators.csrf import csrf_exempt
6
+ from drf_yasg import openapi
7
+ from drf_yasg.utils import swagger_auto_schema
8
+ from rest_framework import mixins, permissions, status, viewsets
9
+ from rest_framework.decorators import action
10
+ from rest_framework.response import Response
11
+
12
+ from ob_dj_store.core.stores.gateway.stripe.models import StripePayment
13
+ from ob_dj_store.core.stores.gateway.stripe.utils import (
14
+ handle_stripe_webhook,
15
+ verify_webhook_signature,
16
+ )
17
+
18
+ from .serializers import (
19
+ CreateStripePaymentSerializer,
20
+ StripeCustomerSerializer,
21
+ StripePaymentSerializer,
22
+ StripePaymentStatusSerializer,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class StripePaymentViewSet(
29
+ mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
30
+ ):
31
+ """Stripe payment management endpoints"""
32
+
33
+ permission_classes = [permissions.IsAuthenticated]
34
+ serializer_class = StripePaymentSerializer
35
+
36
+ def get_queryset(self):
37
+ """Filter stripe payments by current user"""
38
+ return StripePayment.objects.filter(user=self.request.user)
39
+
40
+ def get_serializer_class(self):
41
+ """Return appropriate serializer class based on action"""
42
+ if self.action == "create_payment":
43
+ return CreateStripePaymentSerializer
44
+ elif self.action == "check_status":
45
+ return StripePaymentStatusSerializer
46
+ return self.serializer_class
47
+
48
+ @swagger_auto_schema(
49
+ operation_summary="Create Stripe Payment",
50
+ operation_description="""
51
+ Create a Stripe PaymentIntent for an existing Payment object.
52
+ Returns client_secret for frontend payment confirmation.
53
+ """,
54
+ tags=["Stripe Payment"],
55
+ request_body=CreateStripePaymentSerializer,
56
+ responses={
57
+ 201: openapi.Response(
58
+ description="Payment intent created successfully",
59
+ examples={
60
+ "application/json": {
61
+ "payment_intent_id": "pi_1234567890",
62
+ "client_secret": "pi_1234567890_secret_abc123",
63
+ "status": "requires_payment_method",
64
+ "amount": "25.00",
65
+ "currency": "usd",
66
+ }
67
+ },
68
+ )
69
+ },
70
+ )
71
+ @action(detail=False, methods=["POST"])
72
+ def create_payment(self, request):
73
+ """Create a Stripe PaymentIntent for a payment"""
74
+ serializer = self.get_serializer(data=request.data)
75
+ serializer.is_valid(raise_exception=True)
76
+
77
+ result = serializer.save()
78
+ return Response(result, status=status.HTTP_201_CREATED)
79
+
80
+ @swagger_auto_schema(
81
+ operation_summary="Check Payment Status",
82
+ operation_description="""
83
+ Check the current status of a Stripe payment.
84
+ """,
85
+ tags=["Stripe Payment"],
86
+ request_body=StripePaymentStatusSerializer,
87
+ )
88
+ @action(detail=False, methods=["POST"])
89
+ def check_status(self, request):
90
+ """Check the status of a Stripe payment"""
91
+ serializer = self.get_serializer(data=request.data)
92
+ serializer.is_valid(raise_exception=True)
93
+
94
+ payment_intent_id = serializer.validated_data["payment_intent_id"]
95
+
96
+ try:
97
+ stripe_payment = StripePayment.objects.get(
98
+ payment_intent_id=payment_intent_id, user=request.user
99
+ )
100
+
101
+ payment_serializer = StripePaymentSerializer(stripe_payment)
102
+ return Response(payment_serializer.data)
103
+
104
+ except StripePayment.DoesNotExist:
105
+ return Response(
106
+ {"error": "Payment not found"}, status=status.HTTP_404_NOT_FOUND
107
+ )
108
+
109
+ @swagger_auto_schema(
110
+ operation_summary="Get User's Stripe Customer",
111
+ operation_description="""
112
+ Get or create Stripe customer information for the current user.
113
+ """,
114
+ tags=["Stripe Payment"],
115
+ )
116
+ @action(detail=False, methods=["GET"])
117
+ def customer(self, request):
118
+ """Get user's Stripe customer information"""
119
+ try:
120
+ from ob_dj_store.core.stores.gateway.stripe.utils import (
121
+ get_or_create_stripe_customer,
122
+ )
123
+
124
+ stripe_customer = get_or_create_stripe_customer(request.user)
125
+ serializer = StripeCustomerSerializer(stripe_customer)
126
+ return Response(serializer.data)
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error getting Stripe customer: {e}")
130
+ return Response(
131
+ {"error": "Failed to get customer information"},
132
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
133
+ )
134
+
135
+
136
+ @method_decorator(csrf_exempt, name="dispatch")
137
+ class StripeWebhookViewSet(viewsets.GenericViewSet):
138
+ """Stripe webhook endpoints"""
139
+
140
+ permission_classes = [permissions.AllowAny]
141
+
142
+ @swagger_auto_schema(
143
+ operation_summary="Stripe Webhook Callback",
144
+ operation_description="""
145
+ Handle Stripe webhook events for payment status updates.
146
+ This endpoint is called by Stripe when payment events occur.
147
+ """,
148
+ tags=["Stripe Webhook"],
149
+ )
150
+ @action(detail=False, methods=["POST"])
151
+ def callback(self, request):
152
+ """Handle Stripe webhook callbacks"""
153
+ logger.info(
154
+ f"Received webhook request from IP: {request.META.get('REMOTE_ADDR')}"
155
+ )
156
+ logger.info(f"Request headers: {dict(request.META)}")
157
+
158
+ payload = request.body
159
+ sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
160
+
161
+ logger.info(f"Payload length: {len(payload)}")
162
+ logger.info(f"Signature header present: {bool(sig_header)}")
163
+
164
+ if not sig_header:
165
+ logger.warning("Missing Stripe signature header")
166
+ return HttpResponseBadRequest("Missing signature")
167
+
168
+ try:
169
+ # Verify webhook signature
170
+ event = verify_webhook_signature(payload, sig_header)
171
+ logger.info(f"Received Stripe webhook: {event['type']}")
172
+
173
+ # Handle the webhook
174
+ result = handle_stripe_webhook(event)
175
+
176
+ if result:
177
+ logger.info("Webhook processed successfully")
178
+ return HttpResponse("OK", status=200)
179
+ else:
180
+ logger.error("Failed to process webhook")
181
+ return HttpResponseBadRequest("Failed to process webhook")
182
+
183
+ except ValueError as e:
184
+ logger.error(f"Invalid payload: {e}")
185
+ return HttpResponseBadRequest("Invalid payload")
186
+ except Exception as e:
187
+ logger.error(f"Webhook error: {e}")
188
+ import traceback
189
+
190
+ logger.error(f"Webhook error traceback: {traceback.format_exc()}")
191
+ return HttpResponseBadRequest(str(e))
@@ -28,9 +28,7 @@ class TapPaymentViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
28
28
  operation_description="""
29
29
  gateway_tap callback
30
30
  """,
31
- tags=[
32
- "TAP Payment",
33
- ],
31
+ tags=["TAP Payment",],
34
32
  )
35
33
  @action(detail=False, methods=["POST"], permission_classes=[permissions.AllowAny])
36
34
  def callback(self, request) -> typing.Any:
@@ -55,9 +53,7 @@ class TapPaymentViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
55
53
  operation_description="""
56
54
  Retrieve Tap Transaction from charge id
57
55
  """,
58
- tags=[
59
- "TAP Payment",
60
- ],
56
+ tags=["TAP Payment",],
61
57
  )
62
58
  @action(detail=False, methods=["get"], permission_classes=[permissions.AllowAny])
63
59
  def get(self, request, *args, **kwargs):
@@ -3,8 +3,8 @@ from typing import Any
3
3
 
4
4
  from django import forms
5
5
  from django.contrib import admin
6
- from django.db.models import DecimalField, ExpressionWrapper, F, Sum, Value
7
- from django.db.models.functions import ExtractWeek, Round
6
+ from django.db.models import DecimalField, F, Sum, Value
7
+ from django.db.models.functions import Coalesce, ExtractWeek
8
8
  from django.utils.translation import gettext_lazy as _
9
9
  from import_export.admin import ImportExportModelAdmin
10
10
  from leaflet.admin import LeafletGeoAdmin
@@ -24,6 +24,7 @@ from ob_dj_store.core.stores.admin_inlines import (
24
24
  ProductAttributeInlineAdmin,
25
25
  ProductMediaInlineAdmin,
26
26
  ProductVariantInlineAdmin,
27
+ TipAmountInlineAdmin,
27
28
  )
28
29
 
29
30
 
@@ -84,6 +85,8 @@ class StoreAdmin(LeafletGeoAdmin):
84
85
  "minimum_order_amount",
85
86
  "delivery_charges",
86
87
  "min_free_delivery_amount",
88
+ "created_at",
89
+ "updated_at",
87
90
  ]
88
91
  # define the pickup addresses field as a ManyToManyField
89
92
  # to the address model
@@ -110,7 +113,7 @@ class StoreAdmin(LeafletGeoAdmin):
110
113
  "pickup_addresses",
111
114
  "image",
112
115
  "currency",
113
- "is_open_after_midnight",
116
+ "timezone",
114
117
  )
115
118
  },
116
119
  ),
@@ -140,16 +143,10 @@ class CategoryAdmin(admin.ModelAdmin):
140
143
  ]
141
144
 
142
145
  def get_queryset(self, request):
143
- return (
144
- super()
145
- .get_queryset(request)
146
- .prefetch_related(
147
- "availability_hours",
148
- )
149
- )
146
+ return super().get_queryset(request).prefetch_related("availability_hours",)
150
147
 
151
148
 
152
- class ProductVariantAdmin(admin.ModelAdmin):
149
+ class ProductVariantAdmin(ImportExportModelAdmin, admin.ModelAdmin):
153
150
  inlines = [
154
151
  InventoryInlineAdmin,
155
152
  ProductAttributeInlineAdmin,
@@ -214,13 +211,7 @@ class ProductAdmin(admin.ModelAdmin):
214
211
  return [category.name for category in obj.category.all()]
215
212
 
216
213
  def get_queryset(self, request):
217
- return (
218
- super()
219
- .get_queryset(request)
220
- .prefetch_related(
221
- "category",
222
- )
223
- )
214
+ return super().get_queryset(request).prefetch_related("category",)
224
215
 
225
216
 
226
217
  class ProductAttributeAdmin(admin.ModelAdmin):
@@ -286,25 +277,19 @@ class CartAdmin(admin.ModelAdmin):
286
277
  search_fields = [
287
278
  "customer__email",
288
279
  ]
280
+ autocomplete_fields = ["customer"]
289
281
 
290
282
  def get_queryset(self, request):
291
283
  queryset = super().get_queryset(request)
292
284
  queryset = queryset.annotate(
293
- calculated_total_price=Round(
285
+ calculated_total_price=Coalesce(
294
286
  Sum(
295
- ExpressionWrapper(
296
- (
297
- F("items__product_variant__inventories__price")
298
- - F("items__product_variant__inventories__price")
299
- * F("items__product_variant__inventories__discount_percent")
300
- / 100
301
- ),
302
- output_field=DecimalField(),
303
- )
304
- * F("items__quantity"),
305
- default=Value(0),
287
+ F("items__quantity")
288
+ * F("items__product_variant__inventories__price"),
306
289
  output_field=DecimalField(),
307
- )
290
+ ),
291
+ Value(0),
292
+ output_field=DecimalField(),
308
293
  )
309
294
  )
310
295
  return queryset
@@ -312,7 +297,9 @@ class CartAdmin(admin.ModelAdmin):
312
297
  def calculated_total_price(self, obj):
313
298
  return obj.calculated_total_price
314
299
 
315
- calculated_total_price.admin_order_field = "calculated_total_price"
300
+ calculated_total_price.admin_order_field = (
301
+ "calculated_total_price" # Allow sorting by total_price
302
+ )
316
303
  calculated_total_price.short_description = "Price"
317
304
 
318
305
 
@@ -383,17 +370,13 @@ class OrderAdmin(ImportExportModelAdmin, admin.ModelAdmin):
383
370
  "status",
384
371
  WeekNumberFilter, # Include the filter instance instead of the class name
385
372
  ]
373
+ autocomplete_fields = ["customer"]
386
374
 
387
375
  def get_queryset(self, request):
388
376
  queryset = (
389
377
  super()
390
378
  .get_queryset(request)
391
- .select_related(
392
- "shipping_method",
393
- "payment_method",
394
- "store",
395
- "customer",
396
- )
379
+ .select_related("shipping_method", "payment_method", "store", "customer",)
397
380
  .prefetch_related(
398
381
  "items",
399
382
  "items__product_variant__inventories",
@@ -422,6 +405,7 @@ class PaymentAdmin(ImportExportModelAdmin, admin.ModelAdmin):
422
405
  search_fields = ["orders__store__name", "user__email"]
423
406
  readonly_fields = ("orders",)
424
407
  date_hierarchy = "created_at"
408
+ autocomplete_fields = ["user"]
425
409
 
426
410
 
427
411
  class InventoryAdmin(admin.ModelAdmin):
@@ -532,6 +516,7 @@ class PartnerAdmin(admin.ModelAdmin):
532
516
  inlines = [
533
517
  PartnerEmailDomainInlineAdmin,
534
518
  ]
519
+ ordering = ["created_at"]
535
520
 
536
521
 
537
522
  class DiscountAdmin(admin.ModelAdmin):
@@ -551,6 +536,23 @@ class PartnerAuthInfoAdmin(admin.ModelAdmin):
551
536
  list_filter = ["authentication_expires", "partner"]
552
537
 
553
538
 
539
+ class TipAdmin(admin.ModelAdmin):
540
+ list_display = (
541
+ "id",
542
+ "name",
543
+ "description",
544
+ "is_active",
545
+ "country",
546
+ )
547
+ search_fields = [
548
+ "name",
549
+ ]
550
+ list_filter = ("country",)
551
+ inlines = [
552
+ TipAmountInlineAdmin,
553
+ ]
554
+
555
+
554
556
  admin.site.register(models.Store, StoreAdmin)
555
557
  admin.site.register(models.ShippingMethod, ShippingMethodAdmin)
556
558
  admin.site.register(models.PaymentMethod, PaymentMethodAdmin)
@@ -575,3 +577,4 @@ admin.site.register(models.Partner, PartnerAdmin)
575
577
  admin.site.register(models.Discount, DiscountAdmin)
576
578
  admin.site.register(models.PartnerAuthInfo, PartnerAuthInfoAdmin)
577
579
  admin.site.register(models.CountryPaymentMethod, CountryPaymentMethodAdmin)
580
+ admin.site.register(models.Tip, TipAdmin)
@@ -13,6 +13,7 @@ class OpeningHoursInlineAdmin(admin.TabularInline):
13
13
  "from_hour",
14
14
  "to_hour",
15
15
  "always_open",
16
+ "is_open_after_midnight",
16
17
  ]
17
18
 
18
19
  def get_queryset(self, request):
@@ -26,13 +27,7 @@ class AvailabilityHoursInlineAdmin(admin.TabularInline):
26
27
  fields = ["store", "weekday", "from_hour", "to_hour"]
27
28
 
28
29
  def get_queryset(self, request):
29
- return (
30
- super()
31
- .get_queryset(request)
32
- .select_related(
33
- "store",
34
- )
35
- )
30
+ return super().get_queryset(request).select_related("store",)
36
31
 
37
32
 
38
33
  class PhoneContactInlineAdmin(admin.TabularInline):
@@ -107,12 +102,7 @@ class CartItemInlineAdmin(admin.TabularInline):
107
102
  class OrderItemInline(admin.TabularInline):
108
103
  model = models.OrderItem
109
104
  extra = 0
110
- fields = (
111
- "product_variant",
112
- "quantity",
113
- "unit_value",
114
- "total_amount",
115
- )
105
+ fields = ("product_variant", "quantity", "unit_value", "total_amount", "notes")
116
106
  readonly_fields = (
117
107
  "unit_value",
118
108
  "total_amount",
@@ -130,3 +120,8 @@ class InventoryOperationInlineAdmin(admin.TabularInline):
130
120
  class PartnerEmailDomainInlineAdmin(admin.TabularInline):
131
121
  model = models.PartnerEmailDomain
132
122
  extra = 1
123
+
124
+
125
+ class TipAmountInlineAdmin(admin.TabularInline):
126
+ model = models.TipAmount
127
+ extra = 0
@@ -0,0 +1,2 @@
1
+ # Stripe Payment Gateway
2
+ default_app_config = "ob_dj_store.core.stores.gateway.stripe.apps.StripeConfig"