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_ledger.io.io_core import get_localdate 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.utils.timezone import now from sqlalchemy.orm.base import object_state 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, ItemModel,EstimateModel,InvoiceModel from django_countries.fields import CountryField from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class DealerUserManager(UserManager): def create_user_with_dealer( self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields, ): user = self.create_user( username=email, 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 StaffUserManager(UserManager): def create_user_with_staff( self, email, password, name, arabic_name, phone_number, staff_type, **extra_fields, ): user = self.create_user( username=email, email=email, password=password, **extra_fields ) Staff.objects.create( user=user, name=name, arabic_name=arabic_name, phone_number=phone_number, staff_type=staff_type, **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) def __str__(self): return f"Rate: {self.rate}%" class CarType(models.IntegerChoices): CAR = 1, _("Car") LIGHT_COMMERCIAL = 2, _("Light Commercial") HEAVY_DUTY_TRACTORS = 3, _("Heavy-Duty Tractors") TRAILERS = 4, _("Trailers") MEDIUM_TRUCKS = 5, _("Medium Trucks") BUSES = 6, _("Buses") MOTORCYCLES = 20, _("Motorcycles") BUGGY = 21, _("Buggy") MOTO_ATV = 22, _("Moto ATV") SCOOTERS = 23, _("Scooters") KARTING = 24, _("Karting") ATV = 25, _("ATV") SNOWMOBILES = 26, _("Snowmobiles") class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) is_sa_import = models.BooleanField(default=False) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) 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, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) 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, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True) year_end = models.IntegerField(blank=True, null=True) generation_name = models.CharField(max_length=255, 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, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) start_production_year = models.IntegerField(blank=True, null=True) end_production_year = models.IntegerField(blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Trim" class CarEquipment(models.Model, LocalizedNameMixin): id_car_equipment = models.AutoField(primary_key=True) id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim") name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True) def __str__(self): return self.name class Meta: verbose_name = "Equipment" 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 CarOption(models.Model, LocalizedNameMixin): id_car_option = models.AutoField(primary_key=True) name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) 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 = "Option" class CarOptionValue(models.Model): id_car_option_value = models.AutoField(primary_key=True) id_car_option = models.ForeignKey( CarOption, models.DO_NOTHING, db_column="id_car_option" ) id_car_equipment = models.ForeignKey( CarEquipment, models.DO_NOTHING, db_column="id_car_equipment" ) value = models.CharField(max_length=500) unit = models.CharField(max_length=255, blank=True, null=True) is_base = models.IntegerField() def __str__(self): return f"{self.id_car_option.name}: {self.value} {self.unit}" class Meta: verbose_name = "Option Value" class CarTransferStatusChoices(models.TextChoices): draft = "draft", _("Draft") approved = "approved", _("Approved") pending = "pending", _("Pending") accepted = "accepted", _("Accepted") success = "success", _("Success") reject = "reject", _("Reject") cancelled = "cancelled", _("Cancelled") class CarStatusChoices(models.TextChoices): AVAILABLE = "available", _("Available") SOLD = "sold", _("Sold") HOLD = "hold", _("Hold") DAMAGED = "damaged", _("Damaged") RESERVED = "reserved", _("Reserved") TRANSFER = "transfer", _("Transfer") 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") ) item = models.OneToOneField( ItemModel, on_delete=models.CASCADE, verbose_name=_("Item"), null=True, blank=True, ) def to_dict(self): return { "name": self.name, "price": str(self.price), "price_": str(self.price_), "taxable": self.taxable, "uom": self.uom, } @property def price_(self): vat = VatRate.objects.filter(is_active=True).first() return Decimal(self.price + (self.price * vat.rate)) if self.taxable else self.price 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( VendorModel, 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() def get_transfer(self): return self.transfer_logs.filter(active=True).first() @property def get_car_group(self): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" def to_dict(self): return { "vin": self.vin, "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", "year": self.year, "display_name": self.get_car_group, "status": self.status, "stock_type": self.stock_type, "remarks": self.remarks, "mileage": self.mileage, "receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'), "id": self.id, } class CarTransfer(models.Model): car = models.ForeignKey( "Car", on_delete=models.CASCADE, related_name="transfer_logs", verbose_name=_("Car"), ) from_dealer = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="transfers_out", verbose_name=_("From Dealer"), ) to_dealer = models.ForeignKey( "Dealer", on_delete=models.CASCADE, related_name="transfers_in", verbose_name=_("To Dealer"), ) transfer_date = models.DateTimeField( auto_now_add=True, verbose_name=_("Transfer Date") ) quantity = models.IntegerField(verbose_name=_("Quantity"),default=1) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) status = models.CharField( CarTransferStatusChoices.choices, max_length=10, default=CarTransferStatusChoices.draft, ) is_approved = models.BooleanField(default=False) active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @property def total_price(self): return self.quantity * self.car.finances.total_vat class Meta: verbose_name = _("Car Transfer Log") verbose_name_plural = _("Car Transfer Logs") def __str__(self): return f"{self.car} Transfer car from {self.from_dealer} to {self.to_dealer}" 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")) @property 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( AdditionalServices, 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"), ) @property def total(self): return self.selling_price @property def total_additionals(self): return sum(x.price_ for x in self.additional_services.all()) @property def total_discount(self): if self.discount_amount > 0: return self.selling_price - self.discount_amount return self.selling_price @property def total_vat(self): return round(self.total_discount + self.vat_amount + self.total_additionals,2) @property def vat_amount(self): vat = VatRate.objects.filter(is_active=True).first() if vat: return (self.total_discount * Decimal(vat.rate)).quantize(Decimal("0.01")) return Decimal("0.00") @property def revenue(self): return self.selling_price - self.cost_price def to_dict(self): return { "cost_price": str(self.cost_price), "selling_price": str(self.selling_price), "discount_amount": str(self.discount_amount), "total": str(self.total), "total_discount": str(self.total_discount), "total_vat": str(self.total_vat), "vat_amount": str(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 = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) objects = StaffUserManager() class Meta: verbose_name = _("Staff") verbose_name_plural = _("Staff") permissions = [] def __str__(self): return f"{self.name}" class Sources(models.TextChoices): REFERRALS = "referrals", _("Referrals") WHATSAPP = "whatsapp", _("WhatsApp") SHOWROOM = "showroom", _("Showroom") TIKTOK = "tiktok", _("TikTok") INSTAGRAM = "instagram", _("Instagram") X = "x", _("X") FACEBOOK = "facebook", _("Facebook") MOTORY = "motory", _("Motory") INFLUENCERS = "influencers", _("Influencers") YOUTUBE = "youtube", _("Youtube") CAMPAIGN = "campaign", _("Campaign") class Channel(models.TextChoices): WALK_IN = "walk_in", _("Walk In") TOLL_FREE = "toll_free", _("Toll Free") WEBSITE = "website", _("Website") EMAIL = "email", _("Email") FORM = "form", _("Form") class Status(models.TextChoices): NEW = "new", _("New") PENDING = "pending", _("Pending") IN_PROGRESS = "in_progress", _("In Progress") QUALIFIED = "qualified", _("Qualified") CANCELED = "canceled", _("Canceled") class Title(models.TextChoices): MR = "mr", _("Mr") MRS = "mrs", _("Mrs") MS = "ms", _("Ms") MISS = "miss", _("Miss") DR = "dr", _("Dr") PROF = "prof", _("Prof") PRINCE = "prince", _("Prince") PRINCESS = "princess", _("Princess") COMPANY = "company", _("Company") NA = "na", _("N/A") class ActionChoices(models.TextChoices): CALL = "call", _("Call") SMS = "sms", _("SMS") EMAIL = "email", _("Email") WHATSAPP = "whatsapp", _("WhatsApp") VISIT = "visit", _("Visit") ADD_CAR = "add_car", _("Add Car") RESERVE_CAR = "reserve_car", _("Reserve Car") REMOVE_CAR = "remove_car", _("Remove Car") CREATE_QUOTATION = "create_quotation", _("Create Quotation") CANCEL_QUOTATION = "cancel_quotation", _("Cancel Quotation") CREATE_ORDER = "create_order", _("Create Order") CANCEL_ORDER = "cancel_order", _("Cancel Order") CREATE_INVOICE = "create_invoice", _("Create Invoice") CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice") class Stage(models.TextChoices): PROSPECT = "prospect", _("Prospect") PROPOSAL = "proposal", _("Proposal") NEGOTIATION = "negotiation", _("Negotiation") CLOSED_WON = "closed_won", _("Closed Won") CLOSED_LOST = "closed_lost", _("Closed Lost") class Priority(models.TextChoices): LOW = "low", _("Low") MEDIUM = "medium", _("Medium") HIGH = "high", _("High") class Customer(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="customers" ) title = models.CharField( choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") ) 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")) gender = models.CharField( choices=[("m", _("Male")), ("f", _("Female"))], max_length=1, verbose_name=_("Gender"), ) dob = models.DateField(verbose_name=_("Date of Birth")) 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")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) 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 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 = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) 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, unique=True, 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 Lead(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads") customer = models.ForeignKey( Customer, on_delete=models.CASCADE, related_name="leads" ) id_car_make = models.ForeignKey( CarMake, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Make"), ) id_car_model = models.ForeignKey( CarModel, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Model"), ) year = models.PositiveSmallIntegerField( verbose_name=_("Year"), blank=True, null=True ) source = models.CharField( max_length=50, choices=Sources.choices, verbose_name=_("Source") ) channel = models.CharField( max_length=50, choices=Channel.choices, verbose_name=_("Channel") ) city = models.CharField(max_length=50, verbose_name=_("City")) staff = models.ForeignKey( Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned"), ) priority = models.CharField( max_length=10, choices=Priority.choices, default=Priority.MEDIUM, verbose_name=_("Priority"), ) status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True, default=Status.NEW, ) created = models.DateTimeField( auto_now_add=True, verbose_name=_("Created"), db_index=True ) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Lead") verbose_name_plural = _("Leads") def __str__(self): return f"{self.first_name} {self.last_name}" class LeadStatusHistory(models.Model): lead = models.ForeignKey( Lead, on_delete=models.CASCADE, related_name="status_history" ) old_status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("Old Status") ) new_status = models.CharField( max_length=50, choices=Status.choices, verbose_name=_("New Status") ) changed_by = models.ForeignKey( Staff, on_delete=models.DO_NOTHING, related_name="status_changes" ) changed_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Changed At")) class Meta: verbose_name = _("Lead Status History") verbose_name_plural = _("Lead Status Histories") def __str__(self): return f"{self.lead}: {self.old_status} → {self.new_status}" def validate_probability(value): if value < 0 or value > 100: raise ValidationError(_("Probability must be between 0 and 100.")) class Opportunity(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="opportunities" ) 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") ) stage = models.CharField( max_length=20, choices=Stage.choices, verbose_name=_("Stage") ) status = models.CharField( max_length=20, choices=Status.choices, verbose_name=_("Status"), default=Status.NEW, ) staff = models.ForeignKey( Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner"), ) probability = models.PositiveIntegerField(validators=[validate_probability]) closing_date = models.DateField(verbose_name=_("Closing Date")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) closed = models.BooleanField(default=False, verbose_name=_("Closed")) class Meta: verbose_name = _("Opportunity") verbose_name_plural = _("Opportunities") def __str__(self): return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer.get_full_name}" class Notes(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") note = models.TextField(verbose_name=_("Note")) created_by = models.ForeignKey( User, on_delete=models.DO_NOTHING, related_name="notes_created" ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Note") verbose_name_plural = _("Notes") def __str__(self): return f"Note by {self.created_by.first_name} on {self.content_object}" class Activity(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") activity_type = models.CharField( max_length=50, choices=ActionChoices.choices, verbose_name=_("Activity Type") ) notes = models.TextField(blank=True, null=True, verbose_name=_("Notes")) created_by = models.ForeignKey( User, on_delete=models.DO_NOTHING, related_name="activities_created" ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) class Meta: verbose_name = _("Activity") verbose_name_plural = _("Activities") def __str__(self): return f"{self.get_activity_type_display()} by {self.created_by.name} on {self.content_object}" 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 = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) class Meta: verbose_name = _("Notification") verbose_name_plural = _("Notifications") ordering = ["-created"] 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 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}" class SaleOrder(models.Model): estimate = models.ForeignKey( EstimateModel, on_delete=models.CASCADE, related_name="sale_orders", verbose_name=_("Estimate") ) invoice = models.ForeignKey( InvoiceModel, on_delete=models.CASCADE, related_name="sale_orders", verbose_name=_("Invoice"), null=True, blank=True ) payment_method = models.CharField(max_length=20, choices=[ ('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ]) comments = models.TextField(blank=True, null=True) formatted_order_id = models.CharField(max_length=10, unique=True, editable=False) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created'] def save(self, *args, **kwargs): if not self.formatted_order_id: last_order = SaleOrder.objects.order_by('-id').first() if last_order: next_id = last_order.id + 1 else: next_id = 1 year = get_localdate().year self.formatted_order_id = f"O-{year}-{next_id:09d}" super().save(*args, **kwargs) def __str__(self): return f"Sale Order for {self.full_name}" @property def full_name(self): return f"{self.customer.customer_name}" @property def price(self): return self.car.finances.selling_price @property def items(self): if self.estimate.get_itemtxs_data(): return self.estimate.get_itemtxs_data()[0] return [] @property def customer(self): return self.estimate.customer