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, ) return user UNIT_CHOICES = ( ("Kg", _("Kg")), ("L", _("L")), ("m", _("m")), ("cm", _("cm")), ("m2", _("m2")), ("m3", _("m3")), ("m3", _("m3")), ) 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" # 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 DEALER_TYPES(models.TextChoices): OWNER = "Owner", _("Owner") INVENTORY = "Inventory", _("Inventory") ACCOUNTANT = "Accountent", _("Accountent") SALES = "sales", _("Sales") 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=UNIT_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() # 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(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')) # 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): """Calculate the total amount including VAT.""" return get_total(self) 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})" # 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 #subscription class Subscription(models.Model): plan = models.CharField(max_length=255) # e.g. "basic", "premium" start_date = models.DateField() end_date = models.DateField() max_users = models.IntegerField() # maximum number of users per account users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model is_active = models.BooleanField(default=True) def __str__(self): return self.plan class SubscriptionUser(models.Model): subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): return f"{self.subscription} - {self.user}" class SubscriptionPlan(models.Model): name = models.CharField(max_length=255) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) max_users = models.IntegerField() # maximum number of users per account def __str__(self): return f"{self.name} - {self.price}" # 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") ,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")) parent_dealer = models.ForeignKey( "self", on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("Parent Dealer"), related_name="sub_dealers",) dealer_type = models.CharField(max_length=255, choices=DEALER_TYPES.choices, verbose_name=_("Dealer Type"), default=DEALER_TYPES.OWNER,) 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): """Get the price of the active subscription plan for the dealer.""" active_plan = self.get_active_plan if active_plan: subscription_plan = SubscriptionPlan.objects.filter( name=active_plan.plan ).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 # @receiver(post_save, sender=User) # def create_dealer(instance, created, *args, **kwargs): # if created: # Dealer.objects.create(user=instance) # class STAFF_TYPES(models.TextChoices): # # Owner = "Owner", _("Owner") # MANAGER = "manager", _("Manager") # INVENTORY = "inventory", _("Inventory") # ACCOUNTANT = "accountant", _("Accountant") # SALES = "sales", _("Sales") # RECEPTIONIST = "receptionist", _("Receptionist") # TECHNICIAN = "technician", _("Technician") # DRIVER = "driver", _("Driver") # # # class Staff(models.Model, LocalizedNameMixin): # user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff") # dealer = models.ForeignKey(Dealer, on_delete=models.SET_NULL, null=True, blank=True) # name = models.CharField(max_length=255, verbose_name=_("Name")) # arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) # staff_type = models.CharField(choices=STAFF_TYPES.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 = [] # 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 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")) 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")) 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}"