import itertools from uuid import uuid4 from django.conf import settings from django.db import models, transaction from django.db.models import Sum, F, Count from django.contrib.auth.models import User, UserManager from django.db.models.signals import pre_save, post_save from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django_ledger.models import ( VendorModel, EntityModel, EntityUnitModel, ItemModel, AccountModel, ItemModelAbstract, UnitOfMeasureModel, CustomerModel, ItemModelQuerySet, ) from django.db.models import Sum from decimal import Decimal, InvalidOperation from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.contrib.contenttypes.models import ContentType from django.utils.timezone import now from .utilities.financials import get_financial_value, get_total, get_total_financials from django.db.models import FloatField from .mixins import LocalizedNameMixin from django_ledger.models import EntityModel class DealerUserManager(UserManager): def create_user_with_dealer(self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields): user = self.create_user(email=email, password=password, **extra_fields) Dealer.objects.create(user=user, name=dealer_name,arabic_name=arabic_name, crn=crn, vrn=vrn, address=address, **extra_fields) return user class UnitOfMeasure(models.TextChoices): EACH = 'EA', 'Each' PAIR = 'PR', 'Pair' SET = 'SET', 'Set' GALLON = 'GAL', 'Gallon' LITER = 'L', 'Liter' METER = 'M', 'Meter' KILOGRAM = 'KG', 'Kilogram' HOUR = 'HR', 'Hour' BOX = 'BX', 'Box' ROLL = 'RL', 'Roll' PACKAGE = 'PKG', 'Package' DOZEN = 'DZ', 'Dozen' SQUARE_METER = 'SQ_M', 'Square Meter' PIECE = 'PC', 'Piece' BUNDLE = 'BDL', 'Bundle' class VatRate(models.Model): rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15')) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) @property def vat_rate(self): return self.rate / 100 def __str__(self): return f"Rate: {self.rate}%" class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) is_sa_import = models.BooleanField(default=False) def __str__(self): return self.name class Meta: verbose_name = "Make" class CarModel(models.Model, LocalizedNameMixin): id_car_model = models.AutoField(primary_key=True) id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make") name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) def __str__(self): return self.name class Meta: verbose_name = "Model" class CarSerie(models.Model, LocalizedNameMixin): id_car_serie = models.AutoField(primary_key=True) id_car_model = models.ForeignKey( CarModel, models.DO_NOTHING, db_column="id_car_model" ) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) year_begin = models.IntegerField(blank=True, null=True) year_end = models.IntegerField(blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Series" class CarTrim(models.Model, LocalizedNameMixin): id_car_trim = models.AutoField(primary_key=True) id_car_serie = models.ForeignKey( CarSerie, models.DO_NOTHING, db_column="id_car_serie" ) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) start_production_year = models.IntegerField(blank=True, null=True) end_production_year = models.IntegerField(blank=True, null=True) id_car_model = models.ForeignKey(CarModel, models.DO_NOTHING, db_column='id_car_model', blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Trim" class CarSpecification(models.Model, LocalizedNameMixin): id_car_specification = models.AutoField(primary_key=True) name = models.CharField(max_length=255) arabic_name = models.CharField(max_length=255) id_parent = models.ForeignKey( "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True ) def __str__(self): return self.name class Meta: verbose_name = "Specification" class CarSpecificationValue(models.Model): id_car_specification_value = models.AutoField(primary_key=True) id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim") id_car_specification = models.ForeignKey( CarSpecification, models.DO_NOTHING, db_column="id_car_specification" ) value = models.CharField(max_length=500) unit = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return f"{self.id_car_specification.name}: {self.value} {self.unit}" class Meta: verbose_name = "Specification Value" class CarStatusChoices(models.TextChoices): AVAILABLE = "available", _("Available") SOLD = "sold", _("Sold") HOLD = "hold", _("Hold") DAMAGED = "damaged", _("Damaged") RESERVED = "reserved", _("Reserved") class CarStockTypeChoices(models.TextChoices): NEW = "new", _("New") USED = "used", _("Used") class AdditionalServices(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) description = models.TextField(verbose_name=_("Description")) price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Price")) taxable = models.BooleanField(default=False, verbose_name=_("taxable")) uom = models.CharField(max_length=10, choices=UnitOfMeasure.choices, verbose_name=_("Unit of Measurement")) dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, verbose_name=_("Dealer")) class Meta: verbose_name = _("Additional Services") verbose_name_plural = _("Additional Services") def __str__(self): return self.name + " - " + str(self.price) class Car(models.Model): vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) dealer = models.ForeignKey( "Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer") ) vendor = models.ForeignKey( "Vendor", models.DO_NOTHING, null=True, blank=True, related_name="cars", verbose_name=_("Vendor"), ) id_car_make = models.ForeignKey( CarMake, models.DO_NOTHING, db_column="id_car_make", null=True, blank=True, verbose_name=_("Make"), ) id_car_model = models.ForeignKey( CarModel, models.DO_NOTHING, db_column="id_car_model", null=True, blank=True, verbose_name=_("Model"), ) year = models.IntegerField(verbose_name=_("Year")) id_car_serie = models.ForeignKey( CarSerie, models.DO_NOTHING, db_column="id_car_serie", null=True, blank=True, verbose_name=_("Series"), ) id_car_trim = models.ForeignKey( CarTrim, models.DO_NOTHING, db_column="id_car_trim", null=True, blank=True, verbose_name=_("Trim"), ) status = models.CharField( max_length=10, choices=CarStatusChoices.choices, default=CarStatusChoices.AVAILABLE, verbose_name=_("Status"), ) stock_type = models.CharField( max_length=10, choices=CarStockTypeChoices.choices, default=CarStockTypeChoices.NEW, verbose_name=_("Stock Type"), ) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) class Meta: verbose_name = _("Car") verbose_name_plural = _("Cars") def __str__(self): make = self.id_car_make.name if self.id_car_make else "Unknown Make" model = self.id_car_model.name if self.id_car_model else "Unknown Model" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" return f"{self.year} - {make} - {model} - {trim}" def is_reserved(self): active_reservations = self.reservations.filter(reserved_until__gt=now()) return active_reservations.exists() @property def get_car_group(self): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" # class CarData(models.Model): # vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) # make = models.CharField(max_length=255, verbose_name=_("Make")) # make_ar = models.CharField(max_length=255, verbose_name=_("Make Arabic")) # model = models.CharField(max_length=255, verbose_name=_("Model")) # model_ar = models.CharField(max_length=255, verbose_name=_("Model Arabic")) # year = models.IntegerField(verbose_name=_("Year")) # series = models.CharField(max_length=255,verbose_name=_("Series")) # trim = models.CharField(max_length=255,verbose_name=_("Trim")) # specs = models.JsonField # status = models.CharField( # max_length=10, # choices=CarStatusChoices, # default=CarStatusChoices.AVAILABLE, # verbose_name=_("Status") # ) # stock_type = models.CharField( # max_length=10, # choices=CarStockTypeChoices, # default=CarStockTypeChoices.NEW, # verbose_name=_("Stock Type") # ) # remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) # mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) # receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) class CarReservation(models.Model): car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Car")) reserved_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Reserved By")) reserved_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Reserved At")) reserved_until = models.DateTimeField(verbose_name=_("Reserved Until")) def is_active(self): return self.reserved_until > now() class Meta: unique_together = ('car', 'reserved_until') ordering = ['-reserved_at'] verbose_name = _("Car Reservation") verbose_name_plural = _("Car Reservations") # Car Finance Model class CarFinance(models.Model): additional_services = models.ManyToManyField(ItemModel, related_name="additional_finances",blank=True) car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='finances') cost_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Cost Price")) selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price")) discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), default=Decimal('0.00')) # profit_margin = models.DecimalField(max_digits=14, # decimal_places=2, # verbose_name=_("Profit Margin"), # editable=False) # vat_amount = models.DecimalField(max_digits=14, # decimal_places=2, # verbose_name=_("Vat Amount"), # editable=False,default=Decimal('0.00')) # registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"), # default=Decimal('0.00')) # administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"), # default=Decimal('0.00')) # transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"), # default=Decimal('0.00')) # custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), # default=Decimal('0.00')) @property def total(self): total = 0 if self.additional_services.count() != 0: total_additional_services = sum(x.default_amount for x in self.additional_services.all()) total = self.selling_price + total_additional_services else: total = self.selling_price if self.discount_amount != 0: total = total - self.discount_amount return total @property def vat_amount(self): vat = VatRate.objects.filter(is_active=True).first() return (self.total * vat.vat_rate).quantize(Decimal('0.01')) @property def total_vat(self): return self.total + self.vat_amount def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" # def save(self, *args, **kwargs): # self.full_clean() # try: # price_after_discount = self.selling_price - self.discount_amount # self.profit_margin = price_after_discount - self.cost_price # self.vat_amount = settings.VAT_RATE # except InvalidOperation as e: # raise ValidationError(_("Invalid decimal operation: %s") % str(e)) # super().save(*args, **kwargs) class Meta: verbose_name = _("Car Financial Details") verbose_name_plural = _("Car Financial Details") class ExteriorColors(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB")) class Meta: verbose_name = _("Exterior Colors") verbose_name_plural = _("Exterior Colors") def __str__(self): return f"{self.name} ({self.rgb})" class InteriorColors(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB")) class Meta: verbose_name = _("Interior Colors") verbose_name_plural = _("Interior Colors") def __str__(self): return f"{self.name} ({self.rgb})" class CarColors(models.Model): car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors") exterior = models.ForeignKey( "ExteriorColors", on_delete=models.CASCADE, related_name="colors" ) interior = models.ForeignKey( "InteriorColors", on_delete=models.CASCADE, related_name="colors" ) class Meta: verbose_name = _("Color") verbose_name_plural = _("Colors") unique_together = ("car", "exterior", "interior") def __str__(self): return f"{self.car} ({self.exterior.name}) ({self.interior.name})" class CustomCard(models.Model): car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car")) custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number")) custom_date = models.DateField(verbose_name=_("Custom Date")) class Meta: verbose_name = _("Custom Card") verbose_name_plural = _("Custom Cards") def __str__(self): return f"{self.car} - {self.custom_number}" class CarLocation(models.Model): car = models.OneToOneField( Car, on_delete=models.CASCADE, related_name='location', verbose_name=_("Car") ) owner = models.ForeignKey( 'Dealer', on_delete=models.CASCADE, related_name='owned_cars', verbose_name=_("Owner"), help_text=_("Dealer who owns the car.") ) showroom = models.ForeignKey( 'Dealer', on_delete=models.CASCADE, related_name='showroom_cars', verbose_name=_("Showroom"), help_text=_("Dealer where the car is displayed (can be the owner).") ) description = models.TextField( blank=True, null=True, verbose_name=_("Description"), help_text=_("Optional description about the showroom placement.") ) created_at = models.DateTimeField( auto_now_add=True, verbose_name=_("Created At") ) updated_at = models.DateTimeField( auto_now=True, verbose_name=_("Last Updated") ) class Meta: verbose_name = _("Car Location") verbose_name_plural = _("Car Locations") def __str__(self): return f"Car: {self.car}, Showroom: {self.showroom}, Owner: {self.owner}" def is_owner_showroom(self): """ Returns True if the showroom is the same as the owner. """ return self.owner == self.showroom class CarRegistration(models.Model): car = models.ForeignKey( Car, on_delete=models.CASCADE, related_name="registrations", verbose_name=_("Car"), ) plate_number = models.IntegerField(verbose_name=_("Plate Number")) text1 = models.CharField(max_length=1, verbose_name=_("Text 1")) text2 = models.CharField(max_length=1, verbose_name=_("Text 2")) text3 = models.CharField(max_length=1, verbose_name=_("Text 3")) registration_date = models.DateTimeField(verbose_name=_("Registration Date")) class Meta: verbose_name = _("Registration") verbose_name_plural = _("Registrations") def __str__(self): return f"{self.plate_number} - {self.text1} {self.text2} {self.text3}" # TimestampedModel Abstract Class class TimestampedModel(models.Model): created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: abstract = True class Subscription(models.Model): plan = models.ForeignKey("SubscriptionPlan", on_delete=models.CASCADE, related_name="subscriptions") start_date = models.DateField(help_text="Date when the subscription starts") end_date = models.DateField(help_text="Date when the subscription ends") users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model is_active = models.BooleanField(default=True) billing_cycle = models.CharField( max_length=10, choices=[('monthly', 'Monthly'), ('annual', 'Annual')], default='monthly', help_text="Billing cycle for the subscription" ) last_payment_date = models.DateField(null=True, blank=True, help_text="Date of the last payment made") next_payment_date = models.DateField(null=True, blank=True, help_text="Date of the next payment due") class Meta: verbose_name = _("Subscription") verbose_name_plural = _("Subscriptions") def __str__(self): return self.plan.name @property def total_subscribers(self): return self.users.count() class SubscriptionUser(models.Model): subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) class Meta: verbose_name = _("Subscription User") verbose_name_plural = _("Subscription Users") def __str__(self): return f"{self.subscription} - {self.user}" class SubscriptionPlan(models.Model): name = models.CharField(max_length=100, unique=True, help_text=_("Name of the subscription plan")) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) max_users = models.PositiveIntegerField(help_text=_("Maximum number of users allowed"), default=1) max_inventory_size = models.PositiveIntegerField(help_text=_("Maximum number of cars in inventory"), default=50) support_level = models.CharField( max_length=50, choices=[('basic', 'Basic Support'), ('priority', 'Priority Support'), ('dedicated', 'Dedicated Support')], default='basic', help_text="Level of support provided" ) custom_features = models.JSONField(blank=True, null=True, help_text=_("Additional features specific to this plan")) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _("Subscription Plan") verbose_name_plural = _("Subscription Plans") def __str__(self): return f"{self.name} - {self.price}" class Dealer(models.Model, LocalizedNameMixin): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer") crn = models.CharField(max_length=10, verbose_name=_("Commercial Registration Number") ,null=True ,blank=True) vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"), null=True, blank=True) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) name = models.CharField(max_length=255, verbose_name=_("English Name")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) logo = models.ImageField(upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")) entity = models.ForeignKey(EntityModel, on_delete=models.SET_NULL, null=True, blank=True) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) objects = DealerUserManager() @property def get_active_plan(self): try: return self.user.subscription_set.filter(is_active=True).first() except SubscriptionPlan.DoesNotExist: return None @property def get_plan(self): active_plan = self.get_active_plan if active_plan: subscription_plan = SubscriptionPlan.objects.filter( pk=active_plan.pk ).first() if subscription_plan: return subscription_plan return None class Meta: verbose_name = _("Dealer") verbose_name_plural = _("Dealers") # permissions = [ # ('change_dealer_type', 'Can change dealer type'), # ] def __str__(self): return self.name # @property # def get_sub_dealers(self): # if self.dealer_type == "OWNER": # return self.sub_dealers.all() # return None # # @property # def is_parent(self): # return self.dealer_type == "OWNER" # @property # def get_root_dealer(self): # return self.parent_dealer if self.parent_dealer else self ############################## # Additional staff types for later # COORDINATOR = "coordinator", _("Coordinator") # RECEPTIONIST = "receptionist", _("Receptionist") # AGENT = "agent", _("Agent") # TECHNICIAN = "technician", _("Technician") # DRIVER = "driver", _("Driver") ############################## class StaffTypes(models.TextChoices): MANAGER = "manager", _("Manager") INVENTORY = "inventory", _("Inventory") ACCOUNTANT = "accountant", _("Accountant") SALES = "sales", _("Sales") COORDINATOR = "coordinator", _("Coordinator") RECEPTIONIST = "receptionist", _("Receptionist") AGENT = "agent", _("Agent") class Staff(models.Model, LocalizedNameMixin): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff") name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) staff_type = models.CharField(choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) class Meta: verbose_name = _("Staff") verbose_name_plural = _("Staff") permissions = [] def __str__(self): return f"{self.name} - {self.get_staff_type_display()}" class ActionChoices(models.TextChoices): CREATE = "create", _("Create") UPDATE = "update", _("Update") DELETE = "delete", _("Delete") STATUS_CHANGE = "status_change", _("Status Change") class DealStatus(models.TextChoices): NEW = "new", _("New") PENDING = "pending", _("Pending") CANCELED = "canceled", _("Canceled") COMPLETED = "completed", _("Completed") class Priority(models.TextChoices): LOW = "low", _("Low") MEDIUM = "medium", _("Medium") HIGH = "high", _("High") class Sources(models.TextChoices): REFERRALS = "referrals", _("Referrals") WALK_IN = "walk_in", _("Walk In") TOLL_FREE = "toll_free", _("Toll Free") WHATSAPP = "whatsapp", _("WhatsApp") SHOWROOM = "showroom", _("Showroom") WEBSITE = "website", _("Website") TIKTOK = "tiktok", _("TikTok") INSTAGRAM = "instagram", _("Instagram") X = "x", _("X") FACEBOOK = "facebook", _("Facebook") MOTORY = "motory", _("Motory") INFLUENCERS = "influencers", _("Influencers") YOUTUBE = "youtube", _("Youtube") EMAIL = "email", _("Email") class ContactStatus(models.TextChoices): NEW = "new", _("New") PENDING = "pending", _("Pending") ASSIGNED = "assigned", _("Assigned") CONTACTED = "contacted", _("Contacted") ACCEPTED = "accepted", _("Accepted") QUALIFIED = "qualified", _("Qualified") CANCELED = "canceled", _("Canceled") # class Contact(models.Model): # AGE_RANGES = ( # ('18-30', '18 - 30'), # ('31-40', '31 - 40'), # ('41-50', '41 - 50'), # ('51-60', '51 - 60'), # ('61-70', '61 - 70'), # ('71-80', '71 - 80'), # ('81-90', '81 - 90'), # ) # # dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="contacts") # first_name = models.CharField(max_length=50, verbose_name=_("First Name")) # last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) # age = models.CharField(choices=AGE_RANGES, max_length=20, verbose_name=_("Age")) # gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender")) # phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) # email = models.EmailField(verbose_name=_("Email")) # id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, verbose_name=_("Make")) # id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, verbose_name=_("Model")) # year = models.PositiveSmallIntegerField(verbose_name=_("Year")) # status = models.CharField(choices=ContactStatus.choices, max_length=255, verbose_name=_("Status"), default=ContactStatus.NEW) # created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) # updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) # enquiry_type = models.CharField(choices=[("quotation", _("Quote")),("testdrive", _("Test drive"))], max_length=50, verbose_name=_("Enquiry Type")) # purchase_method = models.CharField(choices=[("c", _("Cash")),("f", _("Finance"))], max_length=1, verbose_name=_("Purchase Method")) # source = models.CharField(max_length=100, choices=Sources.choices, verbose_name=_("Source")) # salary = models.PositiveIntegerField(verbose_name=_("Salary")) # obligations = models.PositiveIntegerField(verbose_name=_("Obligations")) # # class Meta: # verbose_name = _("Contact") # verbose_name_plural = _("Contacts") # # def __str__(self): # return self.first_name + " " + self.last_name class Customer(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="customers") first_name = models.CharField(max_length=50, verbose_name=_("First Name")) middle_name = models.CharField( max_length=50, blank=True, null=True, verbose_name=_("Middle Name") ) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) email = models.EmailField(unique=True, verbose_name=_("Email")) national_id = models.CharField( max_length=10, unique=True, verbose_name=_("National ID") ) phone_number = PhoneNumberField( region="SA", unique=True, verbose_name=_("Phone Number") ) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) is_lead = models.BooleanField(default=False, verbose_name=_("Is Lead")) class Meta: verbose_name = _("Customer") verbose_name_plural = _("Customers") def __str__(self): middle = f" {self.middle_name}" if self.middle_name else "" return f"{self.first_name}{middle} {self.last_name}" @property def get_full_name(self): return f"{self.first_name} {self.middle_name} {self.last_name}" class Opportunity(models.Model): customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="opportunities") car = models.ForeignKey(Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")) deal_name = models.CharField(max_length=255, verbose_name=_("Deal Name")) deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value")) deal_status = models.CharField(max_length=20, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status")) priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority")) created_by = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="deals_created") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) class Meta: verbose_name = _("Opportunity") verbose_name_plural = _("Opportunities") def __str__(self): return f"{self.deal_name} - {self.customer.get_full_name}" class Notes(models.Model): opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="notes") note = models.TextField(verbose_name=_("Note")) created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="notes_created") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) class Meta: verbose_name = _("Notes") verbose_name_plural = _("Notes") class OpportunityLog(models.Model): opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="logs") action = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Action")) staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, verbose_name=_("Staff")) old_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("Old Status")) new_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("New Status")) details = models.TextField(blank=True, null=True, verbose_name=_("Details")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: verbose_name = _("Log") verbose_name_plural = _("Logs") ordering = ['-created_at'] def __str__(self): return f"{self.get_action_display()} by {self.user} on {self.opportunity.deal_name}" class Notification(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications") message = models.CharField(max_length=255, verbose_name=_("Message")) is_read = models.BooleanField(default=False, verbose_name=_("Is Read")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: verbose_name = _("Notification") verbose_name_plural = _("Notifications") ordering = ['-created_at'] def __str__(self): return self.message class Vendor(models.Model, LocalizedNameMixin): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") crn = models.CharField( max_length=10, unique=True, verbose_name=_("Commercial Registration Number") ) vrn = models.CharField( max_length=15, unique=True, verbose_name=_("VAT Registration Number") ) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) name = models.CharField(max_length=255, verbose_name=_("English Name")) contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) logo = models.ImageField( upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: verbose_name = _("Vendor") verbose_name_plural = _("Vendors") def __str__(self): return self.name class Organization(models.Model, LocalizedNameMixin): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations') name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) crn = models.CharField(max_length=15, verbose_name=_("Commercial Registration Number")) vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) logo = models.ImageField(upload_to="logos", blank=True, null=True, verbose_name=_("Logo")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") def __str__(self): return self.name class Representative(models.Model, LocalizedNameMixin): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='representatives') name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) id_number = models.CharField(max_length=10, verbose_name=_("ID Number")) phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) organization = models.ManyToManyField(Organization, related_name='representatives') class Meta: verbose_name = _("Representative") verbose_name_plural = _("Representatives") def __str__(self): return self.name class SaleQuotation(models.Model): quotation_number = models.CharField(max_length=10, unique=True) STATUS_CHOICES = [ ("Draft", _("Draft")), ("Approved", _("Approved")), ("In Review", _("In Review")), ("Paid", _("Paid")), ] dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="sales", null=True ) customer = models.ForeignKey( Customer, on_delete=models.CASCADE, related_name="quotations", verbose_name=_("Customer"), ) amount = models.DecimalField( decimal_places=2, default=Decimal("0.00"), max_digits=10, verbose_name=_("Amount"), ) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) is_approved = models.BooleanField(default=False) status = models.CharField( max_length=10, choices=STATUS_CHOICES, default="Draft", verbose_name=_("Status") ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) posted = models.BooleanField(default=False) payment_id = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Payment ID")) is_paid = models.BooleanField(default=False) date_draft = models.DateTimeField(null=True, blank=True, verbose_name=_('Draft Date')) date_in_review = models.DateTimeField(null=True, blank=True, verbose_name=_('In Review Date')) date_approved = models.DateTimeField(null=True, blank=True, verbose_name=_('Approved Date')) date_paid = models.DateTimeField(null=True, blank=True, verbose_name=_('Paid Date')) date_void = models.DateTimeField(null=True, blank=True, verbose_name=_('Void Date')) date_canceled = models.DateTimeField(null=True, blank=True, verbose_name=_('Canceled Date')) @property def total_quantity(self): total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total'] return total_quantity or 0 @property def total(self): total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) if not total: return 0 return total["total_price"] @property def total_vat(self): if self.total: return float(self.total) * 0.15 + float(self.total) return 0 # def confirm(self): # """Confirm the quotation and lock financial details.""" # if self.status != "DRAFT": # raise ValueError(_("Only draft quotations can be confirmed.")) # self.status = "CONFIRMED" # self.save() # def cancel(self): # """Cancel the quotation.""" # if self.status == "CONFIRMED": # raise ValueError(_("Cannot cancel a confirmed quotation.")) # self.status = "CANCELED" # self.save() def __str__(self): return f"Quotation #{self.quotation_number} for {self.customer}" @property def display_quotation_number(self): return f"QN-{self.quotation_number}" def save(self, *args, **kwargs): if not self.quotation_number: self.quotation_number = str(next(self._get_quotation_number())).zfill(6) super().save(*args, **kwargs) @classmethod def _get_quotation_number(cls): last_quotation = cls.objects.all().order_by('id').last() if last_quotation: last_quotation_number = int(last_quotation.quotation_number) else: last_quotation_number = 0 return itertools.count(last_quotation_number + 1) class SaleQuotationCar(models.Model): quotation = models.ForeignKey( SaleQuotation, on_delete=models.CASCADE, related_name="quotation_cars", verbose_name=_("Quotation"), ) car = models.ForeignKey( Car, on_delete=models.CASCADE, verbose_name=_("Car") ) quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity")) @property def finance(self): return self.car.finances @property def financial_details(self): """ Retrieve financial details dynamically from CarFinance. Returns a dictionary with all financial fields for better access. """ car_finance = self.car.finances if not car_finance: return None return { "selling_price": car_finance.selling_price, "administration_fee": car_finance.administration_fee, "transportation_fee": car_finance.transportation_fee, "custom_card_fee": car_finance.custom_card_fee, "registration_fee": car_finance.registration_fee, "vat_amount": car_finance.vat_amount, # "total_amount": car_finance.total, } @property def total(self): """ Calculate total price dynamically based on quantity and selling price. """ if not self.car.finances: return Decimal("0.00") return self.car.finances.selling_price * self.quantity @property def total_vat(self): """ Calculate total price dynamically based on quantity and selling price. """ if not self.car.finances: return Decimal("0.00") price = float(self.car.finances.selling_price * self.quantity) return (price * 0.15) + price def __str__(self): return f"{self.car} - Quotation #{self.quotation.id}" class SalesOrder(models.Model): quotation = models.OneToOneField( SaleQuotation, on_delete=models.CASCADE, related_name="sales_order", verbose_name=_("Quotation"), ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) total_amount = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Total Amount") ) def __str__(self): return f"Sales Order #{self.id} from Quotation #{self.quotation.id}" class Payment(models.Model): METHOD_CHOICES = [ ('cash', _('cash')), ('credit', _('credit')), ('transfer', _('transfer')), ('debit', _('debit')), ('SADAD', _('SADAD')), ] quotation = models.ForeignKey(SaleQuotation, on_delete=models.CASCADE, related_name="payments") amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("amount")) payment_method = models.CharField(choices=METHOD_CHOICES, max_length=50, verbose_name=_("method")) reference_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("reference number")) payment_date = models.DateField(auto_now_add=True, verbose_name=_("date")) # def save(self, *args, **kwargs): # super().save(*args, **kwargs) # self.quotation.remaining_balance -= self.amount # if self.quotation.remaining_balance <= 0: # self.quotation.is_paid = True # self.quotation.save() class Meta: verbose_name = _("payment") verbose_name_plural = _("payments") def __str__(self): return f"Payment of {self.amount} on {self.payment_date} for {self.quotation}" class Refund(models.Model): payment = models.OneToOneField(Payment, on_delete=models.CASCADE, related_name="refund") amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("amount")) reason = models.TextField(blank=True, verbose_name=_("reason")) refund_date = models.DateField(auto_now_add=True, verbose_name=_("refund date")) class Meta: verbose_name = _("refund") verbose_name_plural = _("refunds") def __str__(self): return f"Refund of {self.amount} on {self.refund_date}" class UserActivityLog(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) action = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "User Activity Log" verbose_name_plural = "User Activity Logs" ordering = ['-timestamp'] def __str__(self): return f"{self.user.email} - {self.action} - {self.timestamp}"