diff --git a/inventory/models.py b/inventory/models.py index eb908558..3b708eed 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -17,7 +17,6 @@ from django_ledger.models import ( UnitOfMeasureModel, CustomerModel, ItemModelQuerySet, - ) from django.db.models import Sum from decimal import Decimal, InvalidOperation @@ -29,66 +28,105 @@ from sqlalchemy.orm.base import object_state from .utilities.financials import get_financial_value, get_total, get_total_financials from django.db.models import FloatField from .mixins import LocalizedNameMixin -from django_ledger.models import EntityModel,ItemModel +from django_ledger.models import EntityModel, ItemModel from django_countries.fields import CountryField from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class DealerUserManager(UserManager): - def create_user_with_dealer(self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields): - user = self.create_user(username=email, email=email, password=password, **extra_fields) - Dealer.objects.create(user=user, name=dealer_name,arabic_name=arabic_name, crn=crn, vrn=vrn, address=address, **extra_fields) + 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) + 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' + 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')) + 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 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): @@ -121,7 +159,9 @@ class CarModel(models.Model, LocalizedNameMixin): 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") + 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) @@ -137,7 +177,9 @@ class CarSerie(models.Model, LocalizedNameMixin): 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") + 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) @@ -159,6 +201,7 @@ class CarEquipment(models.Model, LocalizedNameMixin): def __str__(self): return self.name + class Meta: verbose_name = "Equipment" @@ -167,7 +210,9 @@ 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) + id_parent = models.ForeignKey( + "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True + ) def __str__(self): return self.name @@ -179,7 +224,9 @@ class CarSpecification(models.Model, LocalizedNameMixin): 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") + 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) @@ -194,7 +241,9 @@ 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) + id_parent = models.ForeignKey( + "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True + ) def __str__(self): return self.name @@ -205,8 +254,12 @@ class CarOption(models.Model, LocalizedNameMixin): 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") + 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() @@ -218,27 +271,51 @@ class CarOptionValue(models.Model): 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") + + class CarStatusChoices(models.TextChoices): AVAILABLE = "available", _("Available") SOLD = "sold", _("Sold") HOLD = "hold", _("Hold") DAMAGED = "damaged", _("Damaged") - RESERVED = "reserved", _("Reserved") - + 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")) + 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) + 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, + ) class Meta: verbose_name = _("Additional Services") @@ -325,13 +402,71 @@ class Car(models.Model): 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}" + +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.cost_price + 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")) + 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")) @@ -340,53 +475,64 @@ class CarReservation(models.Model): return self.reserved_until > now() class Meta: - unique_together = ('car', 'reserved_until') - ordering = ['-reserved_at'] + 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')) - + 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): if self.additional_services.count() > 0: - return self.selling_price + sum(x.price for x in self.additional_services.all()) + return self.selling_price + sum( + x.price for x in self.additional_services.all() + ) return self.selling_price + @property def total_discount(self): if self.discount_amount > 0: return self.total - self.discount_amount return self.total - + @property def total_vat(self): return self.total_discount + self.vat_amount - - + @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') + 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 - - + return self.selling_price - self.cost_price + def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" - # def save(self, *args, **kwargs): + # def save(self, *args, **kwargs): # self.full_clean() # try: # price_after_discount = self.selling_price - self.discount_amount @@ -446,7 +592,12 @@ class CarColors(models.Model): class CustomCard(models.Model): - car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car")) + 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")) @@ -460,39 +611,30 @@ class CustomCard(models.Model): class CarLocation(models.Model): car = models.OneToOneField( - Car, - on_delete=models.CASCADE, - related_name='location', - verbose_name=_("Car") + Car, on_delete=models.CASCADE, related_name="location", verbose_name=_("Car") ) owner = models.ForeignKey( - 'Dealer', + "Dealer", on_delete=models.CASCADE, - related_name='owned_cars', + related_name="owned_cars", verbose_name=_("Owner"), - help_text=_("Dealer who owns the car.") + help_text=_("Dealer who owns the car."), ) showroom = models.ForeignKey( - 'Dealer', + "Dealer", on_delete=models.CASCADE, - related_name='showroom_cars', + related_name="showroom_cars", verbose_name=_("Showroom"), - help_text=_("Dealer where the car is displayed (can be the owner).") + 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") + 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") @@ -519,16 +661,14 @@ class CarRegistration(models.Model): 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_type = registration_date = models.DateTimeField(verbose_name=_("Registration Date")) - registration_expiry_date = models.DateTimeField(verbose_name=_("Registration Expiry Date")) class Meta: verbose_name = _("Registration") verbose_name_plural = _("Registrations") def __str__(self): - return f"{self.text1} {self.text2} {self.text3} - {self.plate_number}" + return f"{self.plate_number} - {self.text1} {self.text2} {self.text3}" # TimestampedModel Abstract Class @@ -541,19 +681,28 @@ class TimestampedModel(models.Model): class Subscription(models.Model): - plan = models.ForeignKey("SubscriptionPlan", on_delete=models.CASCADE, related_name="subscriptions") + 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 + 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" + 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") + 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") @@ -579,18 +728,30 @@ class SubscriptionUser(models.Model): class SubscriptionPlan(models.Model): - name = models.CharField(max_length=100, unique=True, help_text=_("Name of the subscription plan")) + 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) + 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" + 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") ) - 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) @@ -604,26 +765,27 @@ class SubscriptionPlan(models.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) + 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) + 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")) @@ -649,14 +811,14 @@ class Dealer(models.Model, LocalizedNameMixin): class Meta: verbose_name = _("Dealer") - verbose_name_plural = _("Dealers") + 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": @@ -670,18 +832,18 @@ class Dealer(models.Model, LocalizedNameMixin): # 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") +# 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") @@ -698,7 +860,9 @@ class Staff(models.Model, LocalizedNameMixin): 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")) + 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")) @@ -726,6 +890,7 @@ class Sources(models.TextChoices): YOUTUBE = "youtube", _("Youtube") CAMPAIGN = "campaign", _("Campaign") + class Channel(models.TextChoices): WALK_IN = "walk_in", _("Walk In") TOLL_FREE = "toll_free", _("Toll Free") @@ -741,6 +906,7 @@ class Status(models.TextChoices): QUALIFIED = "qualified", _("Qualified") CANCELED = "canceled", _("Canceled") + class Title(models.TextChoices): MR = "mr", _("Mr") MRS = "mrs", _("Mrs") @@ -753,6 +919,7 @@ class Title(models.TextChoices): COMPANY = "company", _("Company") NA = "na", _("N/A") + class ActionChoices(models.TextChoices): CALL = "call", _("Call") SMS = "sms", _("SMS") @@ -769,6 +936,7 @@ class ActionChoices(models.TextChoices): CREATE_INVOICE = "create_invoice", _("Create Invoice") CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice") + class Stage(models.TextChoices): PROSPECT = "prospect", _("Prospect") PROPOSAL = "proposal", _("Proposal") @@ -776,6 +944,7 @@ class Stage(models.TextChoices): CLOSED_WON = "closed_won", _("Closed Won") CLOSED_LOST = "closed_lost", _("Closed Lost") + class Priority(models.TextChoices): LOW = "low", _("Low") MEDIUM = "medium", _("Medium") @@ -783,17 +952,33 @@ class Priority(models.TextChoices): class Customer(models.Model): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE,related_name="customers") - title = models.CharField(choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")) + dealer = models.ForeignKey( + Dealer, on_delete=models.CASCADE, related_name="customers" + ) + title = models.CharField( + choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") + ) first_name = models.CharField(max_length=50, verbose_name=_("First Name")) - middle_name = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Middle Name")) + 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")) + 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")) + 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")) @@ -811,33 +996,48 @@ class Customer(models.Model): class Organization(models.Model, LocalizedNameMixin): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations') + 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")) + 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")) + 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') + 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")) + 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') + 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") @@ -849,18 +1049,57 @@ class Representative(models.Model, LocalizedNameMixin): class Lead(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads") - customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="leads") - id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Make")) - id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Model")) - year = models.PositiveSmallIntegerField(verbose_name=_("Year"), blank=True, null=True) - source = models.CharField(max_length=50, choices=Sources.choices, verbose_name=_("Source")) - channel = models.CharField(max_length=50, choices=Channel.choices, verbose_name=_("Channel")) + customer = models.ForeignKey( + Customer, on_delete=models.CASCADE, related_name="leads" + ) + id_car_make = models.ForeignKey( + CarMake, + on_delete=models.DO_NOTHING, + blank=True, + null=True, + verbose_name=_("Make"), + ) + id_car_model = models.ForeignKey( + CarModel, + on_delete=models.DO_NOTHING, + blank=True, + null=True, + verbose_name=_("Model"), + ) + year = models.PositiveSmallIntegerField( + verbose_name=_("Year"), blank=True, null=True + ) + source = models.CharField( + max_length=50, choices=Sources.choices, verbose_name=_("Source") + ) + channel = models.CharField( + max_length=50, choices=Channel.choices, verbose_name=_("Channel") + ) city = models.CharField(max_length=50, verbose_name=_("City")) - staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned")) - priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.MEDIUM, - verbose_name=_("Priority")) - status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True, default=Status.NEW) - created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"), db_index=True) + 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: @@ -868,14 +1107,22 @@ class Lead(models.Model): verbose_name_plural = _("Leads") def __str__(self): - return self.customer.get_full_name + return f"{self.first_name} {self.last_name}" class LeadStatusHistory(models.Model): - lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="status_history") - old_status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Old Status")) - new_status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("New Status")) - changed_by = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="status_changes") + 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: @@ -892,12 +1139,31 @@ def validate_probability(value): class Opportunity(models.Model): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="opportunities") - customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="opportunities") - car = models.ForeignKey(Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")) - stage = models.CharField(max_length=20, choices=Stage.choices, verbose_name=_("Stage")) - status = models.CharField(max_length=20, choices=Status.choices, verbose_name=_("Status"), default=Status.NEW) - staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner")) + dealer = models.ForeignKey( + Dealer, on_delete=models.CASCADE, related_name="opportunities" + ) + customer = models.ForeignKey( + Customer, on_delete=models.CASCADE, related_name="opportunities" + ) + car = models.ForeignKey( + Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car") + ) + stage = models.CharField( + max_length=20, choices=Stage.choices, verbose_name=_("Stage") + ) + status = models.CharField( + max_length=20, + choices=Status.choices, + verbose_name=_("Status"), + default=Status.NEW, + ) + staff = models.ForeignKey( + Staff, + on_delete=models.SET_NULL, + null=True, + related_name="owner", + verbose_name=_("Owner"), + ) probability = models.PositiveIntegerField(validators=[validate_probability]) closing_date = models.DateField(verbose_name=_("Closing Date")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) @@ -915,9 +1181,11 @@ class Opportunity(models.Model): class Notes(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + 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_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")) @@ -932,10 +1200,14 @@ class Notes(models.Model): 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")) + 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_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")) @@ -948,7 +1220,9 @@ class Activity(models.Model): class Notification(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications") + 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")) @@ -956,7 +1230,7 @@ class Notification(models.Model): class Meta: verbose_name = _("Notification") verbose_name_plural = _("Notifications") - ordering = ['-created'] + ordering = ["-created"] def __str__(self): return self.message @@ -991,7 +1265,6 @@ class Vendor(models.Model, LocalizedNameMixin): return self.name - class SaleQuotation(models.Model): quotation_number = models.CharField(max_length=10, unique=True) @@ -1003,7 +1276,7 @@ class SaleQuotation(models.Model): ] dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="sales", null=True - ) + ) customer = models.ForeignKey( Customer, on_delete=models.CASCADE, @@ -1023,35 +1296,47 @@ class SaleQuotation(models.Model): ) 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")) + 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')) + 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'] + 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'))) + 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": @@ -1068,11 +1353,11 @@ class SaleQuotation(models.Model): 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) @@ -1080,7 +1365,7 @@ class SaleQuotation(models.Model): @classmethod def _get_quotation_number(cls): - last_quotation = cls.objects.all().order_by('id').last() + last_quotation = cls.objects.all().order_by("id").last() if last_quotation: last_quotation_number = int(last_quotation.quotation_number) else: @@ -1095,16 +1380,13 @@ class SaleQuotationCar(models.Model): related_name="quotation_cars", verbose_name=_("Quotation"), ) - car = models.ForeignKey( - Car, - on_delete=models.CASCADE, - verbose_name=_("Car") - ) + 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): """ @@ -1133,6 +1415,7 @@ class SaleQuotationCar(models.Model): if not self.car.finances: return Decimal("0.00") return self.car.finances.selling_price * self.quantity + @property def total_vat(self): """ @@ -1165,16 +1448,24 @@ class SalesOrder(models.Model): class Payment(models.Model): METHOD_CHOICES = [ - ('cash', _('cash')), - ('credit', _('credit')), - ('transfer', _('transfer')), - ('debit', _('debit')), - ('SADAD', _('SADAD')), + ("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")) + 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): @@ -1193,8 +1484,12 @@ class Payment(models.Model): 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")) + 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")) @@ -1214,9 +1509,7 @@ class UserActivityLog(models.Model): class Meta: verbose_name = "User Activity Log" verbose_name_plural = "User Activity Logs" - ordering = ['-timestamp'] + ordering = ["-timestamp"] def __str__(self): return f"{self.user.email} - {self.action} - {self.timestamp}" - -