37733-vm/core/models.py
2026-01-23 17:23:29 +00:00

311 lines
13 KiB
Python

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
import random
import string
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')
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}"
class OTPCode(models.Model):
phone_number = models.CharField(max_length=20)
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):
code = ''.join(random.choices(string.digits, k=6))
return OTPCode.objects.create(phone_number=phone_number, 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.ImageField(_('Registration (Front Face)'), upload_to='docs/', blank=True, null=True)
registration_back = models.ImageField(_('Registration (Back Face)'), upload_to='docs/', blank=True, null=True)
driver_license_front = models.ImageField(_('Driver License (Front Face)'), upload_to='docs/', blank=True, null=True)
driver_license_back = models.ImageField(_('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
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)
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)
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
@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)