diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index 1663fc4c..8bbef8ed 100644 Binary files a/inventory/__pycache__/forms.cpython-311.pyc and b/inventory/__pycache__/forms.cpython-311.pyc differ diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index 21a18ed9..4f03a24e 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/urls.cpython-311.pyc b/inventory/__pycache__/urls.cpython-311.pyc index f61334df..3a05f167 100644 Binary files a/inventory/__pycache__/urls.cpython-311.pyc and b/inventory/__pycache__/urls.cpython-311.pyc differ diff --git a/inventory/forms.py b/inventory/forms.py index f5b397b6..92ba04a4 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -16,6 +16,7 @@ from .models import ( Vendor, Customer, Car, + CarTransfer, CarFinance, CustomCard, CarRegistration, @@ -32,13 +33,12 @@ from .models import ( Staff, Opportunity, Priority, Sources, Lead, Activity, Notes, CarModel ) -from django_ledger.models import ItemModel, InvoiceModel,BillModel +from django_ledger.models import ItemModel, InvoiceModel, BillModel from django.forms import ModelMultipleChoiceField, ValidationError, DateInput from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from django.forms import formset_factory - User = get_user_model() @@ -121,7 +121,6 @@ class CustomerForm(forms.ModelForm, AddClassMixin): } - class CarForm( forms.ModelForm, AddClassMixin, @@ -242,6 +241,15 @@ class CarLocationForm(forms.ModelForm): } +class CarTransferForm(forms.ModelForm): + class Meta: + model = CarTransfer + fields = ["car", "to_dealer", "remarks"] + widgets = { + "remarks": forms.Textarea(attrs={"rows": 2, "class": "form-control"}), + } + + # Custom Card Form class CustomCardForm(forms.ModelForm): custom_date = forms.DateTimeField( @@ -379,7 +387,7 @@ class CarSelectionTable(tables.Table): template_name = "django_tables2/bootstrap4.html" -class WizardForm1(forms.Form): +class WizardForm1(forms.Form): email = forms.EmailField( widget=forms.EmailInput( attrs={ @@ -561,8 +569,8 @@ class PaymentForm(forms.Form): label="Payment Method", required=True, ) - payment_date = forms.DateField(label="Payment Date",widget=DateInput(attrs={'type': 'date'}), required=True) - + payment_date = forms.DateField(label="Payment Date", widget=DateInput(attrs={'type': 'date'}), required=True) + def clean_amount(self): invoice = self.cleaned_data['invoice'] bill = self.cleaned_data['bill'] @@ -573,12 +581,11 @@ class PaymentForm(forms.Form): if amount <= 0: raise forms.ValidationError("Payment amount must be greater than 0") if model.is_paid(): - raise forms.ValidationError("Invoice is already paid") + raise forms.ValidationError("Invoice is already paid") if amount > model.amount_due: - raise forms.ValidationError("Payment amount is greater than amount due") - return amount + raise forms.ValidationError("Payment amount is greater than amount due") + return amount - class EmailForm(forms.Form): subject = forms.CharField(max_length=255) @@ -586,6 +593,7 @@ class EmailForm(forms.Form): from_email = forms.EmailField() to_email = forms.EmailField(label="To") + class LeadForm(forms.ModelForm): class Meta: model = Lead @@ -615,6 +623,7 @@ class NoteForm(forms.ModelForm): model = Notes fields = ['note'] + class ActivityForm(forms.ModelForm): class Meta: model = Activity @@ -630,16 +639,17 @@ class OpportunityForm(forms.ModelForm): class InvoiceModelCreateForm(InvoiceModelCreateFormBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.fields['cash_account'].widget = forms.HiddenInput() self.fields['prepaid_account'].widget = forms.HiddenInput() self.fields['unearned_account'].widget = forms.HiddenInput() self.fields['date_draft'] = forms.DateField(widget=DateInput(attrs={'type': 'date'})) - + + class BillModelCreateForm(BillModelCreateFormBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.fields['cash_account'].widget = forms.HiddenInput() self.fields['prepaid_account'].widget = forms.HiddenInput() self.fields['unearned_account'].widget = forms.HiddenInput() diff --git a/inventory/migrations/0007_delete_cartransferlog.py b/inventory/migrations/0007_delete_cartransferlog.py deleted file mode 100644 index c926ee10..00000000 --- a/inventory/migrations/0007_delete_cartransferlog.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-21 12:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0006_cartransferlog_delete_invoicemodelbase'), - ] - - operations = [ - migrations.DeleteModel( - name='CarTransferLog', - ), - ] diff --git a/inventory/models.py b/inventory/models.py index 454f0f31..ae0cc00b 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 @@ -36,41 +35,77 @@ 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) @@ -79,19 +114,19 @@ class VatRate(models.Model): 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') + 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): @@ -124,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) @@ -140,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) @@ -171,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 @@ -183,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) @@ -198,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 @@ -209,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() @@ -222,13 +271,22 @@ 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") - + TRANSFER = "transfer", _("Transfer") class CarStockTypeChoices(models.TextChoices): NEW = "new", _("New") @@ -239,11 +297,25 @@ 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") @@ -330,15 +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")) @@ -347,25 +475,37 @@ 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 @@ -382,8 +522,8 @@ class CarFinance(models.Model): 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): @@ -452,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")) @@ -466,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") @@ -525,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 @@ -547,19 +681,27 @@ 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") @@ -586,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) @@ -611,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")) @@ -705,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")) @@ -795,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")) @@ -823,14 +996,22 @@ 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")) @@ -843,14 +1024,20 @@ class Organization(models.Model, LocalizedNameMixin): 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") @@ -862,21 +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: @@ -884,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: @@ -908,13 +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")) @@ -932,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")) @@ -949,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")) @@ -965,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")) @@ -973,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 @@ -1041,23 +1298,35 @@ class SaleQuotation(models.Model): 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"] @@ -1096,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: @@ -1111,11 +1380,7 @@ 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 @@ -1183,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): @@ -1211,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")) @@ -1232,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}" - - diff --git a/templates/header.html b/templates/header.html index 15a7172c..fd7571f7 100644 --- a/templates/header.html +++ b/templates/header.html @@ -301,7 +301,6 @@ {% include "notifications.html" %} -