from django.db import models from django.contrib.auth.models import User, Group from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.utils.translation import get_language from django.utils import timezone from django.core.files import File import random import string import os from io import BytesIO from PIL import Image class Country(models.Model): name = models.CharField(_('Country Name'), max_length=100) code = models.CharField(_('Country Code'), max_length=10) is_default = models.BooleanField(_('Is Default'), default=False) class Meta: verbose_name = _('Country') verbose_name_plural = _('Countries') def __str__(self): return f"{self.name} (+{self.code})" class City(models.Model): country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='cities') name = models.CharField(_('City Name'), max_length=100) class Meta: verbose_name = _('City') verbose_name_plural = _('Cities') ordering = ['name'] def __str__(self): return f"{self.name} ({self.country.name})" class TruckType(models.Model): name = models.CharField(_('Name (EN)'), max_length=100) name_ar = models.CharField(_('Name (AR)'), max_length=100, blank=True) class Meta: verbose_name = _('Truck Type') verbose_name_plural = _('Truck Types') def __str__(self): if get_language() == 'ar' and self.name_ar: return self.name_ar return self.name class Profile(models.Model): ROLE_CHOICES = ( ('SHIPPER', _('Shipper (Need Goods Moved)')), ('TRUCK_OWNER', _('Truck Owner (Service Provider)')), ('ADMIN', _('Administrator')), ) user = models.OneToOneField(User, on_delete=models.CASCADE) role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='SHIPPER') SUBSCRIPTION_CHOICES = ( ('MONTHLY', _('Monthly')), ('ANNUAL', _('Annual')), ('NONE', _('None')), ) subscription_plan = models.CharField(max_length=20, choices=SUBSCRIPTION_CHOICES, default='NONE') subscription_expiry = models.DateField(null=True, blank=True) is_subscription_active = models.BooleanField(default=False) # New Profile Picture field profile_picture = models.ImageField(_('Profile Picture'), upload_to='profiles/', blank=True, null=True) email_verified = models.BooleanField(default=False) phone_verified = models.BooleanField(default=False) def is_expired(self): if self.subscription_plan == "NONE": return False if not self.is_subscription_active: return True if not self.subscription_expiry: return True return self.subscription_expiry < timezone.now().date() country_code = models.CharField(max_length=5, blank=True, default="966") phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety @property def full_phone_number(self): if not self.phone_number: return "" # Remove any existing leading + from country code and phone number cc = str(self.country_code).replace("+", "").strip() pn = str(self.phone_number).replace("+", "").strip() return f"{cc}{pn}" def __str__(self): return f"{self.user.username} - {self.role}" def save(self, *args, **kwargs): if self.profile_picture: self.profile_picture = self.compress_image(self.profile_picture) super().save(*args, **kwargs) def compress_image(self, image_field): if not image_field: return image_field try: # Check file extension ext = os.path.splitext(image_field.name)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.webp']: return image_field img = Image.open(image_field) if img.mode != 'RGB': img = img.convert('RGB') # Resize if too large max_size = (500, 500) img.thumbnail(max_size, Image.LANCZOS) output = BytesIO() img.save(output, format='JPEG', quality=80, optimize=True) output.seek(0) new_name = os.path.splitext(image_field.name)[0] + '.jpg' return File(output, name=new_name) except Exception as e: # Not an image or other error, return as is return image_field class OTPCode(models.Model): phone_number = models.CharField(max_length=20, null=True, blank=True) email = models.EmailField(null=True, blank=True) code = models.CharField(max_length=6) created_at = models.DateTimeField(auto_now_add=True) is_used = models.BooleanField(default=False) def is_valid(self): # Valid for 10 minutes return not self.is_used and (timezone.now() - self.created_at).total_seconds() < 600 @staticmethod def generate_code(phone_number=None, email=None): code = ''.join(random.choices(string.digits, k=6)) return OTPCode.objects.create(phone_number=phone_number, email=email, code=code) class Truck(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks') # Using a fresh name to avoid conflict with partially created fields truck_type_link = models.ForeignKey(TruckType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Truck Type (New)')) # English fields truck_type = models.CharField(_('Truck Type (EN)'), max_length=100, blank=True) model = models.CharField(_('Model (EN)'), max_length=100) load_capacity = models.CharField(_('Load Capacity (EN)'), max_length=100) color = models.CharField(_('Color (EN)'), max_length=50) # Arabic fields truck_type_ar = models.CharField(_('Truck Type (AR)'), max_length=100, blank=True) model_ar = models.CharField(_('Model (AR)'), max_length=100, blank=True) load_capacity_ar = models.CharField(_('Load Capacity (AR)'), max_length=100, blank=True) color_ar = models.CharField(_('Color (AR)'), max_length=50, blank=True) year = models.PositiveIntegerField(_('Year')) plate_no = models.CharField(_('Plate No'), max_length=50) registration_expiry_date = models.DateField(_('Registration Expiry Date'), null=True, blank=True) # Pictures truck_picture = models.ImageField(_('Truck Picture'), upload_to='trucks/', blank=True, null=True) registration_front = models.FileField(_('Registration (Front Face)'), upload_to='docs/', blank=True, null=True) registration_back = models.FileField(_('Registration (Back Face)'), upload_to='docs/', blank=True, null=True) driver_license_front = models.FileField(_('Driver License (Front Face)'), upload_to='docs/', blank=True, null=True) driver_license_back = models.FileField(_('Driver License (Back Face)'), upload_to='docs/', blank=True, null=True) is_approved = models.BooleanField(_('Is Approved'), default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): if self.truck_type_link: return f"{self.truck_type_link} - {self.plate_no}" return f"{self.truck_type} - {self.plate_no}" @property def display_truck_type(self): if self.truck_type_link: return str(self.truck_type_link) if get_language() == 'ar' and self.truck_type_ar: return self.truck_type_ar return self.truck_type @property def display_model(self): if get_language() == 'ar' and self.model_ar: return self.model_ar return self.model @property def display_load_capacity(self): if get_language() == 'ar' and self.load_capacity_ar: return self.load_capacity_ar return self.load_capacity @property def display_color(self): if get_language() == 'ar' and self.color_ar: return self.color_ar return self.color def save(self, *args, **kwargs): # Compress images if self.truck_picture: self.truck_picture = self.compress_image(self.truck_picture) # For docs, compress only if they are images if self.registration_front: self.registration_front = self.compress_image(self.registration_front) if self.registration_back: self.registration_back = self.compress_image(self.registration_back) if self.driver_license_front: self.driver_license_front = self.compress_image(self.driver_license_front) if self.driver_license_back: self.driver_license_back = self.compress_image(self.driver_license_back) super().save(*args, **kwargs) def compress_image(self, image_field): if not image_field: return image_field try: # Check file extension ext = os.path.splitext(image_field.name)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.webp']: return image_field img = Image.open(image_field) # If already small enough, don't compress (optional, but good for performance) # if image_field.size < 300 * 1024: return image_field if img.mode != 'RGB': img = img.convert('RGB') # Resize if too large max_size = (1200, 1200) img.thumbnail(max_size, Image.LANCZOS) output = BytesIO() img.save(output, format='JPEG', quality=70, optimize=True) output.seek(0) new_name = os.path.splitext(image_field.name)[0] + '.jpg' return File(output, name=new_name) except Exception as e: # Not an image or other error, return as is return image_field class Shipment(models.Model): STATUS_CHOICES = ( ('OPEN', _('Open for Bids')), ('IN_PROGRESS', _('In Progress')), ('COMPLETED', _('Completed')), ('CANCELLED', _('Cancelled')), ) shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipments') description = models.TextField(_('Goods Description')) weight = models.CharField(_('Weight/Volume'), max_length=100) # Using a fresh name required_truck_type_link = models.ForeignKey(TruckType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Required Truck Type')) origin_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, related_name='shipments_origin') origin_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, related_name='shipments_origin') destination_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, related_name='shipments_destination') destination_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, related_name='shipments_destination') origin = models.CharField(_('Origin (Legacy)'), max_length=255, blank=True) destination = models.CharField(_('Destination (Legacy)'), max_length=255, blank=True) delivery_date = models.DateField(_('Requested Delivery Date')) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='OPEN') assigned_truck = models.ForeignKey(Truck, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_shipments') created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.display_origin} to {self.display_destination} - {self.status}" @property def display_origin(self): if self.origin_city and self.origin_country: return f"{self.origin_city.name}, {self.origin_country.name}" return self.origin @property def display_destination(self): if self.destination_city and self.destination_country: return f"{self.destination_city.name}, {self.destination_country.name}" return self.destination class Bid(models.Model): STATUS_CHOICES = ( ('PENDING', _('Pending')), ('ACCEPTED', _('Accepted')), ('REJECTED', _('Rejected')), ) shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE, related_name='bids') truck_owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bids') truck = models.ForeignKey(Truck, on_delete=models.CASCADE) amount = models.DecimalField(_('Offer Amount'), max_digits=10, decimal_places=2) comments = models.TextField(_('Comments'), blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Bid by {self.truck_owner.username} for {self.shipment}" class Message(models.Model): shipment = models.ForeignKey(Shipment, on_delete=models.CASCADE, related_name='messages') sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages') content = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return f"From {self.sender.username} at {self.timestamp}" class WhatsAppConfig(models.Model): api_token = models.CharField(_('Wablas API Token'), max_length=255) secret_key = models.CharField(_('Wablas Secret Key'), max_length=255, blank=True, null=True) is_active = models.BooleanField(_('Is Active'), default=True) admin_phone = models.CharField(_("Admin Notification Phone"), max_length=20, blank=True, null=True, help_text=_("WhatsApp number to receive admin notifications (with country code, e.g., 96812345678)")) class Meta: verbose_name = _('WhatsApp Configuration') verbose_name_plural = _('WhatsApp Configuration') def __str__(self): return str(_("WhatsApp Configuration")) class AppSetting(models.Model): app_name = models.CharField(_('App Name'), max_length=100) logo = models.ImageField(_('Logo'), upload_to='app/', blank=True, null=True) slogan = models.CharField(_('Slogan'), max_length=255, blank=True) registration_number = models.CharField(_('Registration Number'), max_length=100, blank=True) tax_number = models.CharField(_('Tax Number'), max_length=100, blank=True) contact_phone = models.CharField(_('Contact Phone'), max_length=20, blank=True) contact_email = models.EmailField(_('Contact Email'), blank=True) contact_address = models.TextField(_('Contact Address'), blank=True) terms_of_service = models.TextField(_('Terms of Service'), blank=True) privacy_policy = models.TextField(_('Privacy Policy'), blank=True) subscription_enabled = models.BooleanField(_('Enable Subscription Fee'), default=False) thawani_enabled = models.BooleanField(_("Enable Thawani Payment"), default=True) # Shipper Fees shipper_monthly_fee = models.DecimalField(_('Shipper Monthly Fee'), max_digits=10, decimal_places=2, default=0.00) shipper_annual_fee = models.DecimalField(_('Shipper Annual Fee'), max_digits=10, decimal_places=2, default=0.00) # Truck Owner Fees truck_owner_monthly_fee = models.DecimalField(_('Truck Owner Monthly Fee'), max_digits=10, decimal_places=2, default=0.00) truck_owner_annual_fee = models.DecimalField(_('Truck Owner Annual Fee'), max_digits=10, decimal_places=2, default=0.00) class Meta: verbose_name = _('App Setting') verbose_name_plural = _('App Settings') def __str__(self): return self.app_name class Banner(models.Model): title = models.CharField(_('Title (EN)'), max_length=200) title_ar = models.CharField(_('Title (AR)'), max_length=200, blank=True) subtitle = models.CharField(_('Subtitle (EN)'), max_length=255, blank=True) subtitle_ar = models.CharField(_('Subtitle (AR)'), max_length=255, blank=True) image = models.ImageField(_('Banner Image'), upload_to='banners/') link = models.URLField(_('Link URL'), blank=True, null=True, help_text=_("Internal or external URL")) is_active = models.BooleanField(_('Is Active'), default=True) order = models.PositiveIntegerField(_('Order'), default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _('Banner') verbose_name_plural = _('Banners') ordering = ['order', '-created_at'] def __str__(self): return self.title @property def display_title(self): if get_language() == 'ar' and self.title_ar: return self.title_ar return self.title @property def display_subtitle(self): if get_language() == 'ar' and self.subtitle_ar: return self.subtitle_ar return self.subtitle class HomeSection(models.Model): SECTION_TYPES = ( ('SIMPLE', _('Simple Text & Image')), ('FEATURES', _('Features List')), ('CTA', _('Call to Action')), ) title = models.CharField(_('Title (EN)'), max_length=200) title_ar = models.CharField(_('Title (AR)'), max_length=200, blank=True) subtitle = models.CharField(_('Subtitle (EN)'), max_length=255, blank=True) subtitle_ar = models.CharField(_('Subtitle (AR)'), max_length=255, blank=True) content = models.TextField(_('Content (EN)'), blank=True) content_ar = models.TextField(_('Content (AR)'), blank=True) image = models.ImageField(_('Image'), upload_to='home_sections/', blank=True, null=True) order = models.PositiveIntegerField(_('Order'), default=0) is_active = models.BooleanField(_('Is Active'), default=True) section_type = models.CharField(max_length=20, choices=SECTION_TYPES, default='SIMPLE') background_color = models.CharField(max_length=50, default='white', help_text="e.g. white, light, primary") class Meta: verbose_name = _('Home Section') verbose_name_plural = _('Home Sections') ordering = ['order'] def __str__(self): return self.title @property def display_title(self): if get_language() == 'ar' and self.title_ar: return self.title_ar return self.title @property def display_subtitle(self): if get_language() == 'ar' and self.subtitle_ar: return self.subtitle_ar return self.subtitle @property def display_content(self): if get_language() == 'ar' and self.content_ar: return self.content_ar return self.content @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): if hasattr(instance, 'profile'): instance.profile.save() else: Profile.objects.create(user=instance) @receiver(post_save, sender=Profile) def sync_user_groups(sender, instance, **kwargs): """ Automatically syncs Django Groups based on the Profile role. """ # Get or create the group for the current role group, created = Group.objects.get_or_create(name=instance.role) # Get all possible role-based groups to clean up all_role_names = [role[0] for role in Profile.ROLE_CHOICES] # Remove user from other role groups they might be in other_groups = Group.objects.filter(name__in=all_role_names).exclude(name=instance.role) instance.user.groups.remove(*other_groups) # Add user to the correct group instance.user.groups.add(group) class Transaction(models.Model): TRANSACTION_TYPES = ( ('PAYMENT', _('Payment')), ('REFUND', _('Refund')), ) STATUS_CHOICES = ( ('PENDING', _('Pending')), ('COMPLETED', _('Completed')), ('FAILED', _('Failed')), ('CANCELLED', _('Cancelled')), ) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions') amount = models.DecimalField(_('Amount'), max_digits=10, decimal_places=2) transaction_type = models.CharField(max_length=20, choices=TRANSACTION_TYPES, default='PAYMENT') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='COMPLETED') description = models.TextField(_('Description'), blank=True) payment_method = models.CharField(_('Payment Method'), max_length=100, blank=True) reference_number = models.CharField(_('Reference Number'), max_length=100, blank=True) receipt_number = models.CharField(_('Receipt Number'), max_length=20, unique=True, blank=True) session_id = models.CharField(_("Session ID"), max_length=255, blank=True, null=True) payment_status = models.CharField(_("Payment Status"), max_length=50, blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _('Transaction') verbose_name_plural = _('Transactions') ordering = ['-created_at'] def save(self, *args, **kwargs): if not self.receipt_number: # Generate a unique receipt number: REC-YYYYMMDD-XXXX date_str = timezone.now().strftime('%Y%m%d') random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) self.receipt_number = f"REC-{date_str}-{random_str}" super().save(*args, **kwargs) def __str__(self): return f"{self.receipt_number} - {self.user.username} ({self.amount})" class ContactMessage(models.Model): name = models.CharField(_('Name'), max_length=100) email = models.EmailField(_('Email')) subject = models.CharField(_('Subject'), max_length=200) message = models.TextField(_('Message')) created_at = models.DateTimeField(auto_now_add=True) is_read = models.BooleanField(default=False) class Meta: verbose_name = _('Contact Message') verbose_name_plural = _('Contact Messages') ordering = ['-created_at'] def __str__(self): return f"{self.subject} - {self.name}" class Testimonial(models.Model): name = models.CharField(_('Customer Name (EN)'), max_length=100) name_ar = models.CharField(_('Customer Name (AR)'), max_length=100, blank=True) role = models.CharField(_('Customer Role (EN)'), max_length=100, blank=True) role_ar = models.CharField(_('Customer Role (AR)'), max_length=100, blank=True) content = models.TextField(_('Testimony (EN)')) content_ar = models.TextField(_('Testimony (AR)'), blank=True) rating = models.PositiveIntegerField(_('Rating (1-5)'), default=5) image = models.ImageField(_('Customer Image'), upload_to='testimonials/', blank=True, null=True) is_active = models.BooleanField(_('Is Active'), default=True) order = models.PositiveIntegerField(_('Order'), default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _('Testimonial') verbose_name_plural = _('Testimonials') ordering = ['order', '-created_at'] def __str__(self): return self.name @property def display_name(self): if get_language() == 'ar' and self.name_ar: return self.name_ar return self.name @property def display_role(self): if get_language() == 'ar' and self.role_ar: return self.role_ar return self.role @property def display_content(self): if get_language() == 'ar' and self.content_ar: return self.content_ar return self.content