This commit is contained in:
Marwan Alwali 2025-01-22 16:20:54 +03:00
parent e0bb0b4eda
commit 6354f0c326
8 changed files with 505 additions and 230 deletions

View File

@ -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()

View File

@ -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',
),
]

View File

@ -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}"

View File

@ -301,7 +301,6 @@
</div>
</li>
{% include "notifications.html" %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" aria-haspopup="true">
{% if request.LANGUAGE_CODE == 'ar' %}
@ -344,7 +343,7 @@
<div class="text-center pt-4 pb-3">
<div class="avatar avatar-xl">
{% if user.dealer.logo %}
{% if user.dealer.logo %}
<img class="rounded-circle" src="{{ user.dealer.logo.url }}" alt="" />
{% elif user.staff.dealer.logo %}
<img class="rounded-circle" src="{{ user.staff.dealer.logo.url }}" alt="" />

View File

@ -74,7 +74,14 @@
</div>
<tr>
<td class="align-middle ps-3">{{ bill.bill_number }}</td>
<td class="align-middle">{{ bill.bill_status }}</td>
<td class="align-middle">
{% if bill.bill.status == 'draft' %}
<span class="badge badge-phoenix badge-phoenix-warning">
{{ bill.bill_status }}
</span>
{% endif %}
</td>
<td class="align-middle text-end py-3 pe-3">
{{bill.vendor.vendor_name}}
</td>