From 0a784beb415032aba028fa25425eacfd7158ce92 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 31 Jan 2026 02:47:03 +0000 Subject: [PATCH] Autosave: 20260131-024701 --- core/__pycache__/admin.cpython-311.pyc | Bin 17788 -> 20062 bytes core/__pycache__/api_views.cpython-311.pyc | Bin 6123 -> 8534 bytes core/__pycache__/forms.cpython-311.pyc | Bin 31190 -> 32073 bytes core/__pycache__/models.cpython-311.pyc | Bin 27204 -> 30647 bytes core/__pycache__/pricing.cpython-311.pyc | Bin 0 -> 2741 bytes core/__pycache__/urls.cpython-311.pyc | Bin 7470 -> 7655 bytes core/__pycache__/views.cpython-311.pyc | Bin 55612 -> 56839 bytes core/admin.py | 58 ++++- core/api_views.py | 44 +++- core/forms.py | 18 +- ...livery_lat_parcel_delivery_lng_and_more.py | 85 +++++++ ...rcel_delivery_lng_and_more.cpython-311.pyc | Bin 0 -> 4068 bytes core/models.py | 36 ++- core/pricing.py | 70 ++++++ core/templates/core/shipment_request.html | 225 +++++++++++++++--- core/urls.py | 3 +- core/views.py | 41 +++- 17 files changed, 526 insertions(+), 54 deletions(-) create mode 100644 core/__pycache__/pricing.cpython-311.pyc create mode 100644 core/migrations/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.py create mode 100644 core/migrations/__pycache__/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.cpython-311.pyc create mode 100644 core/pricing.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index cc24b2e1e9e57812338acfbcc6433769b3da101a..284a89e8b1d74a91301f3476b61cf2d1b8a15de9 100644 GIT binary patch delta 6596 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 delta 4402 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 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e161775e70dfe26c59e313c7cf5c11ebba317ec4..bc1e7a814752084269043bb86ede4788bdf751d4 100644 GIT binary patch delta 9220 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 delta 6379 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 diff --git a/core/__pycache__/pricing.cpython-311.pyc b/core/__pycache__/pricing.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac1ab6c20968a93e803c43c47301e078acc243fa GIT binary patch literal 2741 zcma)8Z)g-p6ra7@z1z#3iPc17Owx@>)Kv4wYFp9PCNxD#+Yl0|5DM4L?&Pw`-tIX& zF?!uIXYoS?3ny5#kir#gs>Fg{3i(=4u-|Uwz+)j$kbaPUxniZQpZaF+Pw!G~eYw1w zdGp@P`!nx1_uZyV4G5@9NojwH2t8zz3i--|$FIZSDpHZk=_t-kaU2`_bl;Ru^{c$@ zkMmP}T$mC-E?huU0W|pohqgc`6vOe~RV*;NW9w(Zj8L^249lK5A zx=d$G5+5h#jHY9SXJ%yz!nYs(AFqMwRb->{0h>F|+rINY+n;WtY{N3Cz7?3QRwa{& zGRrd2nq6&Vb2iw=_!WqAwGu1(7<5u7#ie{HzwNiVB;+RrXW7Ub36yzLrn%~VwxIfx zVT9^W2N~qF&O)fz+8cDAqqQNPB-qz!{^J4vd=Hpa5)MuURDYdypyExPv>KrusE?<9 z#&%5wRX!!wR=^J0B3r4~g_|HGml`CXcVt~zfNEoj&SEJ>uuLUIBZ`hCRkNsUC|J@A zX88Pt-zolFqH7FZ^9R@!SN;DXI)SY_5~>9WB$9DuDp zPk-zwKQu5j^oZ43d)NxMP$cYjxc<1DaD^G&l&Q16M)BIPAd5E~mI0u|lvni`b*5gM+gW(BQ)Kf0o2*`ZOe`38}#;xwL`hR!%oLYzGKAU+tQ<-_hk5t zS`47b=1kAUqv_E?xT!Q)7bBO%OmjxQyv-3?^J43@E=Sz?wYof;rC%A##`pBrnKl0A zxm)L)&bRLJcf@33PhIbwY1VA(3 zp<|GZY{3qy!DSwLUQGV#b>0M~3&gM_8CqPSnrX=7thA^>+m`RUGz~Z~ubPX-5zpXL zkF&2`>!2ge)n~sXpF)DXtmS9*stIia{iwsS(9M)WozFu@;-9C7mXwQxSkhQeZ$e zMBw#6$)jTcdnH(q2t1b347jKQCjft%RYlFg(gu=Z#-}wyX8sNI39jHJ(2XQ&vgK~C zoz9+A_BOz351hg!okkIf%MB6&9opf4y^DVvW?cLeqPW%3R z`~LMk12;~8vvhrFy`v|4(&^~Wck~xpCW_&}=7u6Fu~0*CGupcS$}8#j0il~Z^Nqa^ z8u#6A+~+j*=NtRe#|www&b6G#oXocz%C#K2d-C2#KSu9F(_=qx?aU7R@W$F<=iq4m z;Ha}})Y7g#JVvRGVUvG`C8QBVU_AU{L(ij>XyHjrio`9fL=5-x5`;Bawg3YlCZ ztp}7@Ev*YAZ-UtzG9VFi8C_GbtHPMTuD-x2zd-VeqTO{xw@Zp{7Zp9OD0*D*Pr1O7 zdXXjd3QOvTa3-+wn8{f(>Kw5UPTb^~Ku$b_lQQ{)j2cHOgp)S;n~b_(I+)YI4T43H Hz{COohbGw= delta 519 zcmaEEz0QhnIWI340}yCNRc3A#p2#P`^ni1t#$JxehdK2o3vj1T$z>ZVuy8<(RxoXfC7qW*6cAjFTfoeYhflCNctXG1KIAqCt$olNrUN zfuyXM29R_UQvj0bV6sC@AIRPzCd(Gg$j|gaXtKPt6q9i9*pldUDCn1g|O%p^fNBfw;pB*?6}VDf;Z5<6IF9OL9a zVp7cUKp8eEki4c8NQXC=od;%5kdg2$yGI_nMx?n1p)4&aaMd83W0szr2iDCc% diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9b92d9c9845fee06b064631c3df1dc09f4dc61ee..2314145fa7cdc386f2b7d46a37af4af4e42a93d5 100644 GIT binary patch delta 9432 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 Bu|xm> diff --git a/core/admin.py b/core/admin.py index d0ce2d0..ac7dd08 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User -from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate +from .models import Profile, Parcel, Country, Governate, City, PlatformProfile, Testimonial, DriverRating, NotificationTemplate, PricingRule from django.utils.translation import gettext_lazy as _ from django.urls import path, reverse from django.shortcuts import render @@ -94,7 +94,7 @@ class CustomUserAdmin(UserAdmin): send_whatsapp_link.allow_tags = True class ParcelAdmin(admin.ModelAdmin): - list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'status', 'payment_status', 'created_at') + list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'distance_km', 'status', 'payment_status', 'created_at') list_filter = ( 'status', 'payment_status', @@ -102,13 +102,32 @@ class ParcelAdmin(admin.ModelAdmin): ) search_fields = ('tracking_number', 'shipper__username', 'receiver_name', 'carrier__username') actions = ['export_as_csv', 'print_parcels', 'export_pdf'] + + fieldsets = ( + (None, { + 'fields': ('tracking_number', 'shipper', 'carrier', 'status', 'payment_status', 'thawani_session_id') + }), + (_('Description'), { + 'fields': ('description', 'receiver_name', 'receiver_phone') + }), + (_('Trip & Pricing'), { + 'fields': ('distance_km', 'weight', 'price', 'platform_fee_percentage', 'platform_fee', 'driver_amount'), + 'description': _('Pricing is calculated based on Distance and Weight.') + }), + (_('Pickup Location'), { + 'fields': ('pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address', 'pickup_lat', 'pickup_lng') + }), + (_('Delivery Location'), { + 'fields': ('delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', 'delivery_lat', 'delivery_lng') + }), + ) def export_as_csv(self, request, queryset): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="parcels_report.csv"' writer = csv.writer(response) - writer.writerow(['Tracking Number', 'Shipper', 'Carrier', 'Price (OMR)', 'Status', 'Payment Status', 'Created At', 'Updated At']) + writer.writerow(['Tracking Number', 'Shipper', 'Carrier', 'Total Price (OMR)', 'Platform Fee (%)', 'Platform Charge (OMR)', 'Driver Amount (OMR)', 'Distance (km)', 'Weight (kg)', 'Status', 'Payment Status', 'Created At']) for obj in queryset: writer.writerow([ @@ -116,10 +135,14 @@ class ParcelAdmin(admin.ModelAdmin): obj.shipper.username if obj.shipper else '', obj.carrier.username if obj.carrier else '', obj.price, + obj.platform_fee_percentage, + obj.platform_fee, + obj.driver_amount, + obj.distance_km, + obj.weight, obj.get_status_display(), obj.get_payment_status_display(), - obj.created_at, - obj.updated_at + obj.created_at ]) return response @@ -145,12 +168,16 @@ class PlatformProfileAdmin(admin.ModelAdmin): (_('General Info'), { 'fields': ('name', 'logo', 'slogan', 'address', 'phone_number', 'registration_number', 'vat_number') }), + (_('Financial Configuration'), { + 'fields': ('platform_fee_percentage', 'enable_payment') + }), + (_('Integrations'), { + 'fields': ('google_maps_api_key',), + 'description': _('API Keys for external services.') + }), (_('Policies'), { 'fields': ('privacy_policy_en', 'privacy_policy_ar', 'terms_conditions_en', 'terms_conditions_ar') }), - (_('Payment Configuration'), { - 'fields': ('enable_payment',) - }), (_('WhatsApp Configuration (Wablas Gateway)'), { 'fields': ('whatsapp_access_token', 'whatsapp_app_secret', 'whatsapp_business_phone_number_id'), 'description': _('Configure your Wablas API connection. Use "Test WhatsApp Configuration" to verify.') @@ -238,6 +265,20 @@ class PlatformProfileAdmin(admin.ModelAdmin): fieldsets += ((_('Tools'), {'fields': ('test_connection_link',)}),) return fieldsets +class PricingRuleAdmin(admin.ModelAdmin): + list_display = ('distance_range', 'weight_range', 'price') + list_filter = ('min_distance', 'min_weight') + search_fields = ('price',) + ordering = ('min_distance', 'min_weight') + + def distance_range(self, obj): + return f"{obj.min_distance} - {obj.max_distance} km" + distance_range.short_description = _("Distance Range") + + def weight_range(self, obj): + return f"{obj.min_weight} - {obj.max_weight} kg" + weight_range.short_description = _("Weight Range") + class CountryAdmin(admin.ModelAdmin): list_display = ('name_en', 'name_ar', 'phone_code') search_fields = ('name_en', 'name_ar', 'phone_code') @@ -257,6 +298,7 @@ admin.site.register(City) admin.site.register(PlatformProfile, PlatformProfileAdmin) admin.site.register(Testimonial, TestimonialAdmin) admin.site.register(DriverRating) +admin.site.register(PricingRule, PricingRuleAdmin) class NotificationTemplateAdmin(admin.ModelAdmin): list_display = ('key', 'description') readonly_fields = ('key', 'description', 'available_variables') diff --git a/core/api_views.py b/core/api_views.py index be583de..267d59e 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -6,6 +6,8 @@ from rest_framework.views import APIView from django.db.models import Q from .models import Parcel, Profile from .serializers import ParcelSerializer, ProfileSerializer, PublicParcelSerializer +from .pricing import calculate_haversine_distance, get_pricing_breakdown +from decimal import Decimal class CustomAuthToken(ObtainAuthToken): def post(self, request, *args, **kwargs): @@ -87,4 +89,44 @@ class PublicParcelTrackView(generics.RetrieveAPIView): serializer_class = PublicParcelSerializer permission_classes = [permissions.AllowAny] queryset = Parcel.objects.all() - lookup_field = 'tracking_number' \ No newline at end of file + lookup_field = 'tracking_number' + +class PriceCalculatorView(APIView): + permission_classes = [permissions.AllowAny] # Allow frontend to query without strict auth if needed, or IsAuthenticated + + def post(self, request): + try: + data = request.data + pickup_lat = data.get('pickup_lat') + pickup_lng = data.get('pickup_lng') + delivery_lat = data.get('delivery_lat') + delivery_lng = data.get('delivery_lng') + weight = data.get('weight') + + if not all([pickup_lat, pickup_lng, delivery_lat, delivery_lng, weight]): + return Response({'error': 'Missing location or weight data.'}, status=status.HTTP_400_BAD_REQUEST) + + weight = Decimal(str(weight)) + + # Calculate Distance + distance_km = calculate_haversine_distance(pickup_lat, pickup_lng, delivery_lat, delivery_lng) + + # Get Breakdown + breakdown = get_pricing_breakdown(distance_km, weight) + + if 'error' in breakdown: + return Response(breakdown, status=status.HTTP_400_BAD_REQUEST) + + response_data = { + 'distance_km': round(float(distance_km), 2), + 'weight_kg': float(weight), + 'price': float(breakdown['price']), + 'platform_fee': float(breakdown['platform_fee']), + 'driver_amount': float(breakdown['driver_amount']), + 'platform_fee_percentage': float(breakdown['platform_fee_percentage']), + } + + return Response(response_data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/core/forms.py b/core/forms.py index a4477c8..c216544 100644 --- a/core/forms.py +++ b/core/forms.py @@ -238,19 +238,31 @@ class ParcelForm(forms.ModelForm): fields = [ 'description', 'weight', 'price', 'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address', + 'pickup_lat', 'pickup_lng', 'delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', + 'delivery_lat', 'delivery_lng', + 'distance_km', 'platform_fee', 'driver_amount', 'platform_fee_percentage', 'receiver_name', 'receiver_phone' ] widgets = { 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}), 'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}), - 'price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), + 'price': forms.TextInput(attrs={'class': 'form-control', 'readonly': 'readonly'}), 'pickup_country': forms.Select(attrs={'class': 'form-control'}), 'pickup_governate': forms.Select(attrs={'class': 'form-control'}), 'pickup_city': forms.Select(attrs={'class': 'form-control'}), 'pickup_address': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Street/Building')}), + 'pickup_lat': forms.HiddenInput(), + 'pickup_lng': forms.HiddenInput(), + 'delivery_lat': forms.HiddenInput(), + 'delivery_lng': forms.HiddenInput(), + 'distance_km': forms.HiddenInput(), + 'platform_fee': forms.HiddenInput(), + 'driver_amount': forms.HiddenInput(), + 'platform_fee_percentage': forms.HiddenInput(), + 'delivery_country': forms.Select(attrs={'class': 'form-control'}), 'delivery_governate': forms.Select(attrs={'class': 'form-control'}), 'delivery_city': forms.Select(attrs={'class': 'form-control'}), @@ -262,7 +274,7 @@ class ParcelForm(forms.ModelForm): labels = { 'description': _('Package Description'), 'weight': _('Weight (kg)'), - 'price': _('Your Offer Price (Bid) (OMR)'), + 'price': _('Calculated Price (OMR)'), 'pickup_country': _('Pickup Country'), 'pickup_governate': _('Pickup Governate'), 'pickup_city': _('Pickup City'), @@ -377,4 +389,4 @@ class DriverRatingForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Reverse choices for CSS star rating logic (5 to 1) to ensure left-to-right filling - self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)] + self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)] \ No newline at end of file diff --git a/core/migrations/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.py b/core/migrations/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.py new file mode 100644 index 0000000..85279c1 --- /dev/null +++ b/core/migrations/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.py @@ -0,0 +1,85 @@ +# Generated by Django 5.2.7 on 2026-01-31 02:03 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0022_notificationtemplate'), + ] + + operations = [ + migrations.CreateModel( + name='PricingRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('min_distance', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Min Distance (km)')), + ('max_distance', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Max Distance (km)')), + ('min_weight', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Min Weight (kg)')), + ('max_weight', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Max Weight (kg)')), + ('price', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Price (OMR)')), + ], + options={ + 'verbose_name': 'Pricing Rule', + 'verbose_name_plural': 'Pricing Rules', + 'ordering': ['min_distance', 'min_weight'], + }, + ), + migrations.AddField( + model_name='parcel', + name='delivery_lat', + field=models.DecimalField(blank=True, decimal_places=16, max_digits=20, null=True, verbose_name='Delivery Latitude'), + ), + migrations.AddField( + model_name='parcel', + name='delivery_lng', + field=models.DecimalField(blank=True, decimal_places=16, max_digits=20, null=True, verbose_name='Delivery Longitude'), + ), + migrations.AddField( + model_name='parcel', + name='distance_km', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='Distance (km)'), + ), + migrations.AddField( + model_name='parcel', + name='driver_amount', + field=models.DecimalField(decimal_places=3, default=Decimal('0.000'), max_digits=10, verbose_name='Driver Amount (OMR)'), + ), + migrations.AddField( + model_name='parcel', + name='pickup_lat', + field=models.DecimalField(blank=True, decimal_places=16, max_digits=20, null=True, verbose_name='Pickup Latitude'), + ), + migrations.AddField( + model_name='parcel', + name='pickup_lng', + field=models.DecimalField(blank=True, decimal_places=16, max_digits=20, null=True, verbose_name='Pickup Longitude'), + ), + migrations.AddField( + model_name='parcel', + name='platform_fee', + field=models.DecimalField(decimal_places=3, default=Decimal('0.000'), max_digits=10, verbose_name='Platform Fee (OMR)'), + ), + migrations.AddField( + model_name='parcel', + name='platform_fee_percentage', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=5, verbose_name='Fee Percentage (%)'), + ), + migrations.AddField( + model_name='platformprofile', + name='google_maps_api_key', + field=models.CharField(blank=True, help_text='API Key for Google Maps (Distance Matrix, Maps JS).', max_length=255, verbose_name='Google Maps API Key'), + ), + migrations.AddField( + model_name='platformprofile', + name='platform_fee_percentage', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), help_text='Percentage deducted from total trip price.', max_digits=5, verbose_name='Platform Fee (%)'), + ), + migrations.AlterField( + model_name='parcel', + name='price', + field=models.DecimalField(decimal_places=3, default=Decimal('0.000'), max_digits=10, verbose_name='Total Price (OMR)'), + ), + ] diff --git a/core/migrations/__pycache__/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.cpython-311.pyc b/core/migrations/__pycache__/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fb15abdc6dcca2dd3f0f6f06743c1c03a31adc5 GIT binary patch literal 4068 zcmbVPOHdoh8J-!vkPrw4SiIPv!7*NjT_YBI-;Y{>{MsxI>vfWf%FfiFZiaz*XnRIR zC;|JhhduhXd&nUt$0>8jagU@b2Oozj9aM8lRZiUGvQK+S{_c@vA!JxG(zN>V|6hOq zWBwlft*Kaj9-L@P*YT5`J zPNAX2enuLD7tg|!3Fh)V;_?FGH9j8_I2?YpClU~yiLS?*04NNf2#W5;9)tE%xPxm| zKs`*dr#XY-D1mx+lZSbOTwfQt{w{K3d-^fupEsEYiqo@a^ zGy$FYOKA7a!xjKacs<^-atxjAzzw5wXdJvWv73Gj(RprA8S?ZIWck#{GablHKfZlx>)&||(xp=)&2}KY z&f3hWt)1&Y?jE6c(flcqFQY3AxwjSNFnSNYk3QJ_@Klk$jD8uzkI=j5?iMIvSK?}u~ZXArfw-ncU;vf-~#~+J)kE+WpGKiq+DZOn%*#FIpp`Fpj0bBq<|f$ z*J>G*HQjZ{0B8Xkn+VVVLoFZ1pcURyHnFa)xs?>%;`gi%8)$L>*IESK_QeR80}Sag zQehsz()4eZZp)Qq!yOX!N2RZQcKGcQm!#CeFZUcO9;ymKeA$|)&`*hgq~OcUNQ_u z1a?ZWbB#HljMLQ!+q8WPORI>lfNcx>3&?(NGfc{cSqab0&dpW&4o4`=HuYl&uc{@( zWxEFs=tG3^S5(t3S#BjlwdX2BIW{Y0*=*oXKT*^R8>J$(w31q8{g!pF7J=hHbFsxR zNfUI{CZ@8AaYbBijHGM05iEbST|y~h@UG>m8a|*$O+}=F%dJUidP??FO^qV4S9Qqv zA8L1f(SpkfvK6T2fu2m){Adzx+m?J@_x( zbEsXqfg*>=1sa<88a9fGi_311n+CQtca3?pGSkKrVN@!>GbgPQ+mu|}RSgL?D@rV9 zXTej4JTWE9ksv*0U>9oGGn9a+NZ;j0>;>$!ywW!zn_k$k3XW zdG&Uef$DZyq27VVk5F5J2Tf^z&W_Gmnxa|=%qMuZxJBrz9cyUExd{e8Y<=ocEz(_! z9t)53*E!mZq~eL^95gR!Q>$k%1=nob)8FHB+ml<-o z8}bBA?{SviXF3e-@E~L(EELl&B~<(3?!2P*rE+9-=? zU9~iO7Ok)v%gSm{i?URSX)Z$V=blf4Lj5_p21EMi#UaqtLp;wzed5n~+}ZYTjhptm cee2O%JRhxd-P=cr>cEBPZ1Lr5L>Q<40p-{$CjbBd literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 6545e00..8158635 100644 --- a/core/models.py +++ b/core/models.py @@ -7,6 +7,7 @@ from django.dispatch import receiver from django.utils import timezone from django.core.exceptions import ValidationError import uuid +from decimal import Decimal class Country(models.Model): name_en = models.CharField(_('Name (English)'), max_length=100) @@ -115,6 +116,21 @@ def save_user_profile(sender, instance, **kwargs): if hasattr(instance, 'profile'): instance.profile.save() +class PricingRule(models.Model): + min_distance = models.DecimalField(_('Min Distance (km)'), max_digits=10, decimal_places=2) + max_distance = models.DecimalField(_('Max Distance (km)'), max_digits=10, decimal_places=2) + min_weight = models.DecimalField(_('Min Weight (kg)'), max_digits=10, decimal_places=2) + max_weight = models.DecimalField(_('Max Weight (kg)'), max_digits=10, decimal_places=2) + price = models.DecimalField(_('Price (OMR)'), max_digits=10, decimal_places=3) + + def __str__(self): + return f"{self.min_distance}-{self.max_distance}km | {self.min_weight}-{self.max_weight}kg = {self.price} OMR" + + class Meta: + verbose_name = _('Pricing Rule') + verbose_name_plural = _('Pricing Rules') + ordering = ['min_distance', 'min_weight'] + class Parcel(models.Model): STATUS_CHOICES = ( ('pending', _('Pending Pickup')), @@ -136,19 +152,31 @@ class Parcel(models.Model): description = models.TextField(_('Description')) weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg")) - price = models.DecimalField(_('Price (OMR)'), max_digits=10, decimal_places=3, default=0.000) + price = models.DecimalField(_('Total Price (OMR)'), max_digits=10, decimal_places=3, default=Decimal('0.000')) + # Financial Breakdown + platform_fee = models.DecimalField(_('Platform Fee (OMR)'), max_digits=10, decimal_places=3, default=Decimal('0.000')) + platform_fee_percentage = models.DecimalField(_('Fee Percentage (%)'), max_digits=5, decimal_places=2, default=Decimal('0.00')) + driver_amount = models.DecimalField(_('Driver Amount (OMR)'), max_digits=10, decimal_places=3, default=Decimal('0.000')) + + # Trip Info + distance_km = models.DecimalField(_('Distance (km)'), max_digits=10, decimal_places=2, default=Decimal('0.00')) + # Pickup Location pickup_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Country')) pickup_governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Governate')) pickup_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup City')) pickup_address = models.CharField(_('Pickup Address'), max_length=255) + pickup_lat = models.DecimalField(_('Pickup Latitude'), max_digits=20, decimal_places=16, null=True, blank=True) + pickup_lng = models.DecimalField(_('Pickup Longitude'), max_digits=20, decimal_places=16, null=True, blank=True) # Delivery Location delivery_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Country')) delivery_governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Governate')) delivery_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_city_parcels', verbose_name=_('Delivery City')) delivery_address = models.CharField(_('Delivery Address'), max_length=255) + delivery_lat = models.DecimalField(_('Delivery Latitude'), max_digits=20, decimal_places=16, null=True, blank=True) + delivery_lng = models.DecimalField(_('Delivery Longitude'), max_digits=20, decimal_places=16, null=True, blank=True) receiver_name = models.CharField(_('Receiver Name'), max_length=100) receiver_phone = models.CharField(_('Receiver Phone'), max_length=20) @@ -181,6 +209,12 @@ class PlatformProfile(models.Model): registration_number = models.CharField(_('Registration Number'), max_length=100, blank=True) vat_number = models.CharField(_('VAT Number'), max_length=100, blank=True) + # Financial Configuration + platform_fee_percentage = models.DecimalField(_('Platform Fee (%)'), max_digits=5, decimal_places=2, default=Decimal('0.00'), help_text=_("Percentage deducted from total trip price.")) + + # Integrations + google_maps_api_key = models.CharField(_('Google Maps API Key'), max_length=255, blank=True, help_text=_("API Key for Google Maps (Distance Matrix, Maps JS).")) + # Bilingual Policies privacy_policy_en = models.TextField(_('Privacy Policy (English)'), blank=True) privacy_policy_ar = models.TextField(_('Privacy Policy (Arabic)'), blank=True) diff --git a/core/pricing.py b/core/pricing.py new file mode 100644 index 0000000..cebedb5 --- /dev/null +++ b/core/pricing.py @@ -0,0 +1,70 @@ +from decimal import Decimal +import math +from .models import PricingRule, PlatformProfile + +def calculate_haversine_distance(lat1, lon1, lat2, lon2): + """ + Calculate the great circle distance in kilometers between two points + on the earth (specified in decimal degrees) + """ + if lat1 is None or lon1 is None or lat2 is None or lon2 is None: + return Decimal('0.00') + + # Convert decimal degrees to radians + lat1, lon1, lat2, lon2 = map(float, [lat1, lon1, lat2, lon2]) + + # Haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 + c = 2 * math.asin(math.sqrt(a)) + r = 6371 # Radius of earth in kilometers. Use 3956 for miles + + return Decimal(c * r) + +def get_pricing_breakdown(distance_km, weight_kg): + """ + Returns a dictionary with pricing breakdown: + { + 'price': Decimal, + 'platform_fee': Decimal, + 'platform_fee_percentage': Decimal, + 'driver_amount': Decimal, + 'error': str (optional) + } + """ + # 1. Find matching rule + # We look for a rule that covers this distance and weight + rule = PricingRule.objects.filter( + min_distance__lte=distance_km, + max_distance__gte=distance_km, + min_weight__lte=weight_kg, + max_weight__gte=weight_kg + ).first() + + if not rule: + # Fallback or Error + # Try to find a rule just by distance if weight is slightly off? No, strict for now. + return { + 'price': Decimal('0.000'), + 'platform_fee': Decimal('0.000'), + 'platform_fee_percentage': Decimal('0.00'), + 'driver_amount': Decimal('0.000'), + 'error': 'No pricing rule found for this distance/weight combination.' + } + + total_price = rule.price + + # 2. Calculate Fees + profile = PlatformProfile.objects.first() + fee_percentage = profile.platform_fee_percentage if profile else Decimal('0.00') + + platform_fee = total_price * (fee_percentage / Decimal('100.00')) + driver_amount = total_price - platform_fee + + return { + 'price': total_price, + 'platform_fee': platform_fee, + 'platform_fee_percentage': fee_percentage, + 'driver_amount': driver_amount + } diff --git a/core/templates/core/shipment_request.html b/core/templates/core/shipment_request.html index 8c50555..ec569d4 100644 --- a/core/templates/core/shipment_request.html +++ b/core/templates/core/shipment_request.html @@ -15,8 +15,26 @@

{% trans "Request a Shipment" %}

-
+ + {% if not google_maps_api_key %} +
+ {% trans "Map integration is disabled (API Key missing). Distance must be entered manually or functionality is limited." %} +
+ {% endif %} + + {% csrf_token %} + + + {{ form.pickup_lat }} + {{ form.pickup_lng }} + {{ form.delivery_lat }} + {{ form.delivery_lng }} + {{ form.distance_km }} + {{ form.platform_fee }} + {{ form.driver_amount }} + {{ form.platform_fee_percentage }} +
@@ -36,6 +54,7 @@
{{ form.price }} + {% trans "Calculated automatically based on distance and weight." %} {% if form.price.errors %}
{{ form.price.errors }}
{% endif %} @@ -43,68 +62,58 @@
-

{% trans "Pickup Details" %}

+

+ {% trans "Pickup Details" %} + {% if google_maps_api_key %} + + {% endif %} +

{{ form.pickup_country }} - {% if form.pickup_country.errors %} -
{{ form.pickup_country.errors }}
- {% endif %}
{{ form.pickup_governate }} - {% if form.pickup_governate.errors %} -
{{ form.pickup_governate.errors }}
- {% endif %}
{{ form.pickup_city }} - {% if form.pickup_city.errors %} -
{{ form.pickup_city.errors }}
- {% endif %}
{{ form.pickup_address }} - {% if form.pickup_address.errors %} -
{{ form.pickup_address.errors }}
- {% endif %}
-

{% trans "Delivery Details" %}

+

+ {% trans "Delivery Details" %} + {% if google_maps_api_key %} + + {% endif %} +

{{ form.delivery_country }} - {% if form.delivery_country.errors %} -
{{ form.delivery_country.errors }}
- {% endif %}
{{ form.delivery_governate }} - {% if form.delivery_governate.errors %} -
{{ form.delivery_governate.errors }}
- {% endif %}
{{ form.delivery_city }} - {% if form.delivery_city.errors %} -
{{ form.delivery_city.errors }}
- {% endif %}
{{ form.delivery_address }} - {% if form.delivery_address.errors %} -
{{ form.delivery_address.errors }}
- {% endif %}
@@ -114,9 +123,6 @@
{{ form.receiver_name }} - {% if form.receiver_name.errors %} -
{{ form.receiver_name.errors }}
- {% endif %}
@@ -128,12 +134,6 @@ {{ form.receiver_phone }}
- {% if form.receiver_phone_code.errors %} -
{{ form.receiver_phone_code.errors }}
- {% endif %} - {% if form.receiver_phone.errors %} -
{{ form.receiver_phone.errors }}
- {% endif %}
@@ -147,6 +147,159 @@
+ + + +{% if google_maps_api_key %} + + +{% endif %} +