from uuid import uuid4 from django.conf import settings from django.db import models, transaction from django.contrib.auth.models import User 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 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 .mixins import LocalizedNameMixin 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) 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) 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" # Car Model 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 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, 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 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 selling_price(self): finance = self.finances.first() return finance.selling_price if finance else Decimal('0.00') @property def discount_amount(self): finance = self.finances.first() return finance.discount_amount if finance else Decimal('0.00') @property def vat_amount(self): finance = self.finances.first() return finance.vat_amount if finance else Decimal('0.00') @property def total(self): finance = self.finances.first() return finance.total if finance else Decimal('0.00') 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): 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")) 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) discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), 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')) total = models.DecimalField(max_digits=14, decimal_places=2, default=Decimal('0.00'), null=True, blank=True) def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" def save(self, *args, **kwargs): vat_rate = settings.VAT_RATE self.full_clean() try: services = self.administration_fee + self.transportation_fee + self.custom_card_fee price_after_discount = self.selling_price - self.discount_amount total_vat_amount = (price_after_discount + services) * vat_rate self.vat_amount = price_after_discount * vat_rate self.profit_margin = self.selling_price - self.cost_price - self.discount_amount - self.registration_fee self.total = price_after_discount + services + total_vat_amount + self.registration_fee except InvalidOperation as e: raise ValidationError(_("Invalid decimal operation: %s") % str(e)) super().save(*args, **kwargs) @property def total_vat_amount(self): vat_rate = settings.VAT_RATE services = self.administration_fee + self.transportation_fee + self.custom_card_fee price_after_discount = self.selling_price - self.discount_amount return (price_after_discount + services) * vat_rate 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})" # Colors Model 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})" # Custom Card Model 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 # Car Registration Model 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 # Dealer Model 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")) vrn = models.CharField(max_length=15, 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")) 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")) class Meta: verbose_name = _("Dealer") verbose_name_plural = _("Dealers") def __str__(self): return self.name # Vendor Model 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")) 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")) class Meta: verbose_name = _("Vendor") verbose_name_plural = _("Vendors") def __str__(self): return self.name # Customer Model 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")) 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 SaleQuotation(models.Model): STATUS_CHOICES = [ ("DRAFT", _("Draft")), ("CONFIRMED", _("Confirmed")), ("CANCELED", _("Canceled")), ] 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")) 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")) 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.id} for {self.customer}" 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") ) # dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="sale_cars", verbose_name=_("Dealer")) quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity")) # price = models.DecimalField(decimal_places=2, max_digits=10, verbose_name=_("Price"), editable=False) def get_financial_details(self): """Retrieve financial details dynamically from CarFinance.""" car_finance = self.car.finances.first() 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_rate": car_finance.vat_rate, "discount_amount": car_finance.discount_amount, "total_amount": car_finance.total, } 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}"