from decimal import Decimal import hashlib from django.db import models from datetime import timedelta from django.contrib.auth.models import User, UserManager from django.utils.translation import gettext_lazy as _ from django_ledger.models import ( VendorModel, EntityModel, ItemModel, CustomerModel, ) from django_ledger.io.io_core import get_localdate from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.utils.timezone import now from .mixins import LocalizedNameMixin from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel 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")) hash = models.CharField(max_length=64, blank=True, null=True, verbose_name=_("Hash")) def save(self, *args, **kwargs): self.hash = self.get_hash super(Car, self).save(*args, **kwargs) 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}" @property def get_hash(self): hash_object = hashlib.sha256() color = "" try: color = self.colors.first().exterior.name if self.colors.exists() else "" except: pass hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}{self.year}{self.id_car_serie.name}{self.id_car_trim.name}{color}".encode('utf-8')) return hash_object.hexdigest() def mark_as_sold(self): self.cancel_reservation() self.status = CarStatusChoices.SOLD self.save() def cancel_reservation(self): if self.reservations.exists(): self.reservations.all().delete() def cancel_transfer(self): if self.transfer_logs.exists(): self.transfer_logs.all().delete() 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'), 'hash': self.get_hash, "id": self.id, } def get_specifications(self): specs = CarSpecificationValue.objects.filter(id_car_trim=self.id_car_trim) return specs 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.OneToOneField( 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"), null=True, blank=True) text3 = models.CharField(max_length=1, verbose_name=_("Text 3"), null=True, blank=True) 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}" # 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" ) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile') 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") first_name = models.CharField(max_length=50, verbose_name=_("First Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) email = models.EmailField(verbose_name=_("Email")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) customer = models.ForeignKey( CustomerModel, on_delete=models.CASCADE, related_name="leads", null=True,blank=True ) # car = models.ForeignKey( # Car, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Car") # ) 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") ) address = models.CharField(max_length=50, verbose_name=_("address")) 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}" @property def is_converted(self): return bool(self.customer) def to_dict(self): return { "first_name": str(self.first_name), "last_name": str(self.last_name), "email": str(self.email), "address": str(self.address), "phone_number": str(self.phone_number), "make": str(self.id_car_make.name), "model": str(self.id_car_model.name), "created_at": str(self.created.strftime("%Y-%m-%d")), } @property def full_name(self): return f"{self.first_name} {self.last_name}" def convert_to_customer(self,entity): customer = entity.get_customers().filter(email=self.email).first() if entity and not customer: customer = entity.create_customer( customer_model_kwargs={ "customer_name": self.full_name, "address_1": self.address, "phone": self.phone_number, "email": self.email, } ) customer.additional_info = {} customer.additional_info.update({"info":self.to_dict()}) self.customer = customer self.status = Status.QUALIFIED customer.save() self.save() return customer def get_latest_schedule(self): return self.schedules.order_by('-scheduled_at').first() class Schedule(models.Model): PURPOSE_CHOICES = [ ('Product Demo', 'Product Demo'), ('Follow-Up Call', 'Follow-Up Call'), ('Contract Discussion', 'Contract Discussion'), ('Sales Meeting', 'Sales Meeting'), ('Support Call', 'Support Call'), ('Other', 'Other'), ] ScheduledType = [ ('Call', 'Call'), ('Meeting', 'Meeting'), ('Email', 'Email'), ] ScheduleStatusChoices = [ ('Scheduled', 'Scheduled'), ('Completed', 'Completed'), ('Canceled', 'Canceled'), ] lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='schedules') customer = models.ForeignKey(CustomerModel, on_delete=models.CASCADE, related_name='schedules',null=True,blank=True) scheduled_by = models.ForeignKey(Staff, on_delete=models.CASCADE) purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES) scheduled_at = models.DateTimeField() scheduled_type = models.CharField(max_length=200, choices=ScheduledType,default='Call') duration = models.DurationField(default=timedelta(minutes=5)) notes = models.TextField(blank=True, null=True) status = models.CharField(max_length=200, choices=ScheduleStatusChoices, default='Scheduled') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"Scheduled {self.purpose} with {self.lead.full_name} on {self.scheduled_at}" @property def schedule_past_date(self): if self.scheduled_at < now(): return True return False class Meta: ordering = ['-scheduled_at'] 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( CustomerModel, 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"), ) lead = models.OneToOneField("Lead",related_name="opportunity", on_delete=models.CASCADE,null=True,blank=True) probability = models.PositiveIntegerField(validators=[validate_probability]) closing_date = models.DateField(verbose_name=_("Closing Date"),null=True,blank=True) 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")) estimate = models.OneToOneField(EstimateModel, related_name="opportunity",on_delete=models.SET_NULL,null=True,blank=True) class Meta: verbose_name = _("Opportunity") verbose_name_plural = _("Opportunities") def __str__(self): return f"Opportunity for {self.customer.customer_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.get_full_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