diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index cc24b2e..284a89e 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/api_views.cpython-311.pyc b/core/__pycache__/api_views.cpython-311.pyc index b9df1fd..1311734 100644 Binary files a/core/__pycache__/api_views.cpython-311.pyc and b/core/__pycache__/api_views.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 9c79858..88156a0 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e161775..bc1e7a8 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/pricing.cpython-311.pyc b/core/__pycache__/pricing.cpython-311.pyc new file mode 100644 index 0000000..ac1ab6c Binary files /dev/null and b/core/__pycache__/pricing.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index d628931..30866b8 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9b92d9c..2314145 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ 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 0000000..3fb15ab Binary files /dev/null and b/core/migrations/__pycache__/0023_pricingrule_parcel_delivery_lat_parcel_delivery_lng_and_more.cpython-311.pyc differ 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 %} +