from uuid import uuid4 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 phonenumber_field.modelfields import PhoneNumberField from django.contrib.contenttypes.models import ContentType from decimal import Decimal 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") 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.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 selling_price(self): finance = self.finances.first() return finance.selling_price 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" ) reserved_by = models.ForeignKey(User, on_delete=models.CASCADE) reserved_at = models.DateTimeField(auto_now_add=True) reserved_until = models.DateTimeField() def is_active(self): return self.reserved_until > now() class Meta: unique_together = ("car", "reserved_until") ordering = ["-reserved_at"] # Car Finance Model class CarFinance(models.Model): car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="finances") cost_price = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Cost Price") ) profit_margin = models.DecimalField( max_digits=10, decimal_places=2, verbose_name=_("Profit Margin") ) selling_price = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Selling Price"), editable=False ) vat_rate = models.DecimalField( max_digits=10, decimal_places=2, default=0.15, verbose_name=_("VAT Rate") ) vat_amount = models.DecimalField( max_digits=12, decimal_places=2, verbose_name=_("VAT Amount"), editable=False ) total = models.DecimalField( max_digits=14, decimal_places=2, verbose_name=_("Total Amount"), editable=False ) class Meta: verbose_name = _("Car Financial Details") def save(self, *args, **kwargs): self.full_clean() self.selling_price = self.cost_price * (1 + self.profit_margin) self.vat_amount = self.selling_price * self.vat_rate self.total = self.selling_price + self.vat_amount super().save(*args, **kwargs) def __str__(self): return ( f"Car Financial Details for {self.car}: Selling Price {self.selling_price}" ) # Colors Model class CarColors(models.Model): class ColorType(models.TextChoices): EXTERIOR = "exterior", _("Exterior") INTERIOR = "interior", _("Interior") car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors") 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")) color_type = models.CharField( max_length=10, choices=ColorType.choices, default=ColorType.EXTERIOR, verbose_name=_("Color Type"), ) class Meta: verbose_name = _("Color") verbose_name_plural = _("Colors") def __str__(self): return f"{self.get_color_type_display()} - {self.name} ({self.rgb})" # Custom Card Model class CustomCard(models.Model): car = models.ForeignKey( 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}" # 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") ) 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}" # Create Entity @receiver(post_save, sender=Dealer) def create_ledger_entity(sender, instance, created, **kwargs): if created: entity = EntityModel.objects.create( name=instance.name, admin=instance.user, address_1=instance.address, fy_start_month=1, accrual_method=True, depth=0, ) default_coa = entity.create_chart_of_accounts(assign_as_default=True, commit=True, coa_name=_("Chart of Accounts")) if default_coa: entity.populate_default_coa(activate_accounts=True, coa_model=default_coa) print(f"Ledger entity created for Dealer: {instance.name}") # # Create Vendor @receiver(post_save, sender=Vendor) def create_ledger_vendor(sender, instance, created, **kwargs): if created: entity = EntityModel.objects.filter(name=instance.dealer.name).first() vendor = VendorModel.objects.update_or_create( entity_model=entity, vendor_name=instance.name, vendor_number=instance.crn, address_1=instance.address, phone=instance.phone_number, tax_id_number=instance.vrn, active=True, hidden=False, additional_info={ "arabic_name": instance.arabic_name, "contact_person": instance.contact_person, }, ) print(f"VendorModel created for Vendor: {instance.name}") @receiver(post_save, sender=Customer) def create_customer(sender, instance, created, **kwargs): if created: entity = EntityModel.objects.filter(name=instance.dealer.name).first() name = f"{instance.first_name} {instance.middle_name} {instance.last_name}" customer = CustomerModel.objects.create( entity_model=entity, customer_name=name, customer_number=instance.national_id, address_1=instance.address, phone=instance.phone_number, email=instance.email, sales_tax_rate=0.15, ) print(f"Customer created: {name}") # # Create Item # @receiver(post_save, sender=Car) # def create_item_model(sender, instance, created, **kwargs): # item_name = f"{instance.year} - {instance.id_car_make} - {instance.id_car_model} - {instance.id_car_trim}" # uom_name = _("Car") # unit_abbr = _("C") # # uom, uom_created = UnitOfMeasureModel.objects.get_or_create( # name=uom_name, # unit_abbr=unit_abbr # ) # # if uom_created: # print(f"UOM created: {uom_name}") # else: # print(f"Using existing UOM: {uom_name}") # # entity = EntityModel.objects.filter(name=instance.dealer.name).first() # # inventory_account = AccountModel.objects.first() # cogs_account = AccountModel.objects.first() # earnings_account = AccountModel.objects.first() # # item = ItemModel.objects.create( # entity=entity, # uom=uom, # name=item_name, # item_role=ItemModelAbstract.ITEM_ROLE_INVENTORY, # item_type=ItemModelAbstract.ITEM_TYPE_MATERIAL, # item_id=instance.vin, # sold_as_unit=True, # inventory_received=1.00, # inventory_received_value=0.00, # inventory_account=inventory_account, # for_inventory=True, # is_product_or_service=True, # cogs_account=cogs_account, # earnings_account=earnings_account, # is_active=True, # additional_info={ # "remarks": instance.remarks, # "status": instance.status, # "stock_type": instance.stock_type, # "mileage": instance.mileage, # }, # ) # # print(f"ItemModel {'created' if created else 'updated'} for Car: {item.name}") # # # # update price - CarFinance # @receiver(post_save, sender=CarFinance) # def update_item_model_cost(sender, instance, created, **kwargs): # # ItemModel.objects.filter(item_id=instance.car.vin).update( # inventory_received_value=instance.cost_price, # default_amount=instance.cost_price, # ) # print(f"Inventory item updated with CarFinance data for Car: {instance.car}")