821 lines
29 KiB
Python
821 lines
29 KiB
Python
from uuid import uuid4
|
|
from django.conf import settings
|
|
from django.db import models, transaction
|
|
from django.contrib.auth.models import User
|
|
from django.db.models.signals import pre_save, post_save
|
|
from django.dispatch import receiver
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django_ledger.models import (
|
|
VendorModel,
|
|
EntityModel,
|
|
EntityUnitModel,
|
|
ItemModel,
|
|
AccountModel,
|
|
ItemModelAbstract,
|
|
UnitOfMeasureModel,
|
|
CustomerModel,
|
|
ItemModelQuerySet,
|
|
)
|
|
from django.db.models import Sum
|
|
from decimal import Decimal, InvalidOperation
|
|
from django.core.exceptions import ValidationError
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils.timezone import now
|
|
|
|
from .mixins import LocalizedNameMixin
|
|
|
|
|
|
class CarMake(models.Model, LocalizedNameMixin):
|
|
id_car_make = models.AutoField(primary_key=True)
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
|
|
is_sa_import = models.BooleanField(default=False)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "Make"
|
|
|
|
|
|
class CarModel(models.Model, LocalizedNameMixin):
|
|
id_car_model = models.AutoField(primary_key=True)
|
|
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "Model"
|
|
|
|
|
|
class CarSerie(models.Model, LocalizedNameMixin):
|
|
id_car_serie = models.AutoField(primary_key=True)
|
|
id_car_model = models.ForeignKey(
|
|
CarModel, models.DO_NOTHING, db_column="id_car_model"
|
|
)
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
year_begin = models.IntegerField(blank=True, null=True)
|
|
year_end = models.IntegerField(blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "Series"
|
|
|
|
|
|
class CarTrim(models.Model, LocalizedNameMixin):
|
|
id_car_trim = models.AutoField(primary_key=True)
|
|
id_car_serie = models.ForeignKey(
|
|
CarSerie, models.DO_NOTHING, db_column="id_car_serie"
|
|
)
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
start_production_year = models.IntegerField(blank=True, null=True)
|
|
end_production_year = models.IntegerField(blank=True, null=True)
|
|
id_car_model = models.ForeignKey(CarModel, models.DO_NOTHING, db_column='id_car_model', blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "Trim"
|
|
|
|
|
|
class CarSpecification(models.Model, LocalizedNameMixin):
|
|
id_car_specification = models.AutoField(primary_key=True)
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
id_parent = models.ForeignKey(
|
|
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = "Specification"
|
|
|
|
|
|
class CarSpecificationValue(models.Model):
|
|
id_car_specification_value = models.AutoField(primary_key=True)
|
|
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim")
|
|
id_car_specification = models.ForeignKey(
|
|
CarSpecification, models.DO_NOTHING, db_column="id_car_specification"
|
|
)
|
|
value = models.CharField(max_length=500)
|
|
unit = models.CharField(max_length=255, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.id_car_specification.name}: {self.value} {self.unit}"
|
|
|
|
class Meta:
|
|
verbose_name = "Specification Value"
|
|
|
|
|
|
# Car Model
|
|
class CarStatusChoices(models.TextChoices):
|
|
AVAILABLE = "available", _("Available")
|
|
SOLD = "sold", _("Sold")
|
|
HOLD = "hold", _("Hold")
|
|
DAMAGED = "damaged", _("Damaged")
|
|
RESERVED = "reserved", _("Reserved")
|
|
|
|
|
|
class CarStockTypeChoices(models.TextChoices):
|
|
NEW = "new", _("New")
|
|
USED = "used", _("Used")
|
|
|
|
|
|
class DEALER_TYPES(models.TextChoices):
|
|
Owner = "Owner", _("Owner")
|
|
Inventory = "Inventory", _("Inventory")
|
|
Accountent = "Accountent", _("Accountent")
|
|
Sales = "sales", _("Sales")
|
|
|
|
|
|
class Car(models.Model):
|
|
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
|
|
dealer = models.ForeignKey(
|
|
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
|
|
)
|
|
|
|
vendor = models.ForeignKey(
|
|
"Vendor",
|
|
models.DO_NOTHING,
|
|
null=True,
|
|
blank=True,
|
|
related_name="cars",
|
|
verbose_name=_("Vendor"),
|
|
)
|
|
id_car_make = models.ForeignKey(
|
|
CarMake,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_make",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Make"),
|
|
)
|
|
id_car_model = models.ForeignKey(
|
|
CarModel,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_model",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Model"),
|
|
)
|
|
year = models.IntegerField(verbose_name=_("Year"))
|
|
id_car_serie = models.ForeignKey(
|
|
CarSerie,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_serie",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Series"),
|
|
)
|
|
id_car_trim = models.ForeignKey(
|
|
CarTrim,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_trim",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Trim"),
|
|
)
|
|
status = models.CharField(
|
|
max_length=10,
|
|
choices=CarStatusChoices.choices,
|
|
default=CarStatusChoices.AVAILABLE,
|
|
verbose_name=_("Status"),
|
|
)
|
|
stock_type = models.CharField(
|
|
max_length=10,
|
|
choices=CarStockTypeChoices.choices,
|
|
default=CarStockTypeChoices.NEW,
|
|
verbose_name=_("Stock Type"),
|
|
)
|
|
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
|
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
|
|
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Car")
|
|
verbose_name_plural = _("Cars")
|
|
|
|
def __str__(self):
|
|
make = self.id_car_make.name if self.id_car_make else "Unknown Make"
|
|
model = self.id_car_model.name if self.id_car_model else "Unknown Model"
|
|
trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
|
|
return f"{self.year} - {make} - {model} - {trim}"
|
|
|
|
def is_reserved(self):
|
|
active_reservations = self.reservations.filter(reserved_until__gt=now())
|
|
return active_reservations.exists()
|
|
|
|
@property
|
|
def finance(self):
|
|
"""Fetch the first related CarFinance object, or return None."""
|
|
return self.finances # Assuming a related_name of 'finances'
|
|
|
|
def _get_financial_value(self, attribute):
|
|
"""Reusable method to safely get financial values."""
|
|
finance = self.finance
|
|
return getattr(finance, attribute, Decimal('0.00')) if finance else Decimal('0.00')
|
|
|
|
def _calculate_vat(self, value):
|
|
"""Helper to calculate VAT dynamically for a given value."""
|
|
vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate
|
|
return (value * vat_rate).quantize(Decimal('0.01'))
|
|
|
|
@property
|
|
def cost_price(self):
|
|
return self._get_financial_value('cost_price')
|
|
|
|
@property
|
|
def selling_price(self):
|
|
return self._get_financial_value('selling_price')
|
|
|
|
@property
|
|
def registration_fee(self):
|
|
return self._get_financial_value('registration_fee')
|
|
|
|
@property
|
|
def administration_fee(self):
|
|
return self._get_financial_value('administration_fee')
|
|
|
|
@property
|
|
def transportation_fee(self):
|
|
return self._get_financial_value('transportation_fee')
|
|
|
|
@property
|
|
def custom_card_fee(self):
|
|
return self._get_financial_value('custom_card_fee')
|
|
|
|
@property
|
|
def discount_amount(self):
|
|
return self._get_financial_value('discount_amount')
|
|
|
|
@property
|
|
def vat_amount(self):
|
|
"""Dynamically calculate VAT for the selling price after discount."""
|
|
price_after_discount = self.selling_price - self.discount_amount
|
|
return self._calculate_vat(price_after_discount)
|
|
|
|
@property
|
|
def administration_fee_vat(self):
|
|
return self._calculate_vat(self.administration_fee)
|
|
|
|
@property
|
|
def transportation_fee_vat(self):
|
|
return self._calculate_vat(self.transportation_fee)
|
|
|
|
@property
|
|
def custom_card_fee_vat(self):
|
|
return self._calculate_vat(self.custom_card_fee)
|
|
|
|
@property
|
|
def total_vat_amount(self):
|
|
"""Sum up the VAT for all applicable fields."""
|
|
return (
|
|
self.vat_amount +
|
|
self.administration_fee_vat +
|
|
self.transportation_fee_vat +
|
|
self.custom_card_fee_vat
|
|
)
|
|
|
|
@property
|
|
def total(self):
|
|
"""Calculate the total amount including VAT."""
|
|
price_after_discount = self.selling_price - self.discount_amount
|
|
subtotal = (
|
|
price_after_discount +
|
|
self.registration_fee +
|
|
self.administration_fee +
|
|
self.transportation_fee +
|
|
self.custom_card_fee
|
|
)
|
|
return subtotal + self.total_vat_amount
|
|
|
|
# class CarData(models.Model):
|
|
# vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
|
|
# make = models.CharField(max_length=255, verbose_name=_("Make"))
|
|
# make_ar = models.CharField(max_length=255, verbose_name=_("Make Arabic"))
|
|
# model = models.CharField(max_length=255, verbose_name=_("Model"))
|
|
# model_ar = models.CharField(max_length=255, verbose_name=_("Model Arabic"))
|
|
# year = models.IntegerField(verbose_name=_("Year"))
|
|
# series = models.CharField(max_length=255,verbose_name=_("Series"))
|
|
# trim = models.CharField(max_length=255,verbose_name=_("Trim"))
|
|
# specs = models.JsonField
|
|
# status = models.CharField(
|
|
# max_length=10,
|
|
# choices=CarStatusChoices,
|
|
# default=CarStatusChoices.AVAILABLE,
|
|
# verbose_name=_("Status")
|
|
# )
|
|
# stock_type = models.CharField(
|
|
# max_length=10,
|
|
# choices=CarStockTypeChoices,
|
|
# default=CarStockTypeChoices.NEW,
|
|
# verbose_name=_("Stock Type")
|
|
# )
|
|
# remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
|
# mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
|
|
# receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
|
|
|
|
|
|
|
|
class CarReservation(models.Model):
|
|
car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Car"))
|
|
reserved_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Reserved By"))
|
|
reserved_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Reserved At"))
|
|
reserved_until = models.DateTimeField(verbose_name=_("Reserved Until"))
|
|
|
|
def is_active(self):
|
|
return self.reserved_until > now()
|
|
|
|
class Meta:
|
|
unique_together = ('car', 'reserved_until')
|
|
ordering = ['-reserved_at']
|
|
verbose_name = _("Car Reservation")
|
|
verbose_name_plural = _("Car Reservations")
|
|
|
|
|
|
# Car Finance Model
|
|
class CarFinance(models.Model):
|
|
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"))
|
|
profit_margin = models.DecimalField(max_digits=14,
|
|
decimal_places=2,
|
|
verbose_name=_("Profit Margin"),
|
|
editable=False)
|
|
vat_amount = models.DecimalField(max_digits=14,
|
|
decimal_places=2,
|
|
verbose_name=_("Vat Amount"),
|
|
editable=False)
|
|
discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"),
|
|
default=Decimal('0.00'))
|
|
registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"),
|
|
default=Decimal('0.00'))
|
|
administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"),
|
|
default=Decimal('0.00'))
|
|
transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"),
|
|
default=Decimal('0.00'))
|
|
custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"),
|
|
default=Decimal('0.00'))
|
|
|
|
def __str__(self):
|
|
return f"Car: {self.car}, Selling Price: {self.selling_price}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# vat_rate = settings.VAT_RATE
|
|
self.full_clean()
|
|
try:
|
|
price_after_discount = self.selling_price - self.discount_amount
|
|
self.profit_margin = price_after_discount - self.cost_price
|
|
except InvalidOperation as e:
|
|
raise ValidationError(_("Invalid decimal operation: %s") % str(e))
|
|
super().save(*args, **kwargs)
|
|
|
|
class Meta:
|
|
verbose_name = _("Car Financial Details")
|
|
verbose_name_plural = _("Car Financial Details")
|
|
|
|
|
|
class ExteriorColors(models.Model, LocalizedNameMixin):
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Exterior Colors")
|
|
verbose_name_plural = _("Exterior Colors")
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.rgb})"
|
|
|
|
|
|
class InteriorColors(models.Model, LocalizedNameMixin):
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Interior Colors")
|
|
verbose_name_plural = _("Interior Colors")
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.rgb})"
|
|
|
|
|
|
# Colors Model
|
|
class CarColors(models.Model):
|
|
car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors")
|
|
exterior = models.ForeignKey(
|
|
"ExteriorColors", on_delete=models.CASCADE, related_name="colors"
|
|
)
|
|
interior = models.ForeignKey(
|
|
"InteriorColors", on_delete=models.CASCADE, related_name="colors"
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Color")
|
|
verbose_name_plural = _("Colors")
|
|
unique_together = ("car", "exterior", "interior")
|
|
|
|
def __str__(self):
|
|
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
|
|
|
|
|
|
# Custom Card Model
|
|
class CustomCard(models.Model):
|
|
car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car"))
|
|
custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number"))
|
|
custom_date = models.DateField(verbose_name=_("Custom Date"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Custom Card")
|
|
verbose_name_plural = _("Custom Cards")
|
|
|
|
def __str__(self):
|
|
return f"{self.car} - {self.custom_number}"
|
|
|
|
|
|
class CarLocation(models.Model):
|
|
car = models.OneToOneField(
|
|
Car,
|
|
on_delete=models.CASCADE,
|
|
related_name='location',
|
|
verbose_name=_("Car")
|
|
)
|
|
owner = models.ForeignKey(
|
|
'Dealer',
|
|
on_delete=models.CASCADE,
|
|
related_name='owned_cars',
|
|
verbose_name=_("Owner"),
|
|
help_text=_("Dealer who owns the car.")
|
|
)
|
|
showroom = models.ForeignKey(
|
|
'Dealer',
|
|
on_delete=models.CASCADE,
|
|
related_name='showroom_cars',
|
|
verbose_name=_("Showroom"),
|
|
help_text=_("Dealer where the car is displayed (can be the owner).")
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Description"),
|
|
help_text=_("Optional description about the showroom placement.")
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name=_("Created At")
|
|
)
|
|
updated_at = models.DateTimeField(
|
|
auto_now=True,
|
|
verbose_name=_("Last Updated")
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Car Location")
|
|
verbose_name_plural = _("Car Locations")
|
|
|
|
def __str__(self):
|
|
return f"Car: {self.car}, Showroom: {self.showroom}, Owner: {self.owner}"
|
|
|
|
def is_owner_showroom(self):
|
|
"""
|
|
Returns True if the showroom is the same as the owner.
|
|
"""
|
|
return self.owner == self.showroom
|
|
|
|
# Car Registration Model
|
|
class CarRegistration(models.Model):
|
|
car = models.ForeignKey(
|
|
Car,
|
|
on_delete=models.CASCADE,
|
|
related_name="registrations",
|
|
verbose_name=_("Car"),
|
|
)
|
|
plate_number = models.IntegerField(verbose_name=_("Plate Number"))
|
|
text1 = models.CharField(max_length=1, verbose_name=_("Text 1"))
|
|
text2 = models.CharField(max_length=1, verbose_name=_("Text 2"))
|
|
text3 = models.CharField(max_length=1, verbose_name=_("Text 3"))
|
|
registration_date = models.DateTimeField(verbose_name=_("Registration Date"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Registration")
|
|
verbose_name_plural = _("Registrations")
|
|
|
|
def __str__(self):
|
|
return f"{self.plate_number} - {self.text1} {self.text2} {self.text3}"
|
|
|
|
|
|
# TimestampedModel Abstract Class
|
|
class TimestampedModel(models.Model):
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
#subscription
|
|
class Subscription(models.Model):
|
|
plan = models.CharField(max_length=255) # e.g. "basic", "premium"
|
|
start_date = models.DateField()
|
|
end_date = models.DateField()
|
|
max_users = models.IntegerField() # maximum number of users per account
|
|
users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model
|
|
is_active = models.BooleanField(default=True)
|
|
def __str__(self):
|
|
return self.plan
|
|
class SubscriptionUser(models.Model):
|
|
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
def __str__(self):
|
|
return f"{self.subscription} - {self.user}"
|
|
|
|
class SubscriptionPlan(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
description = models.TextField()
|
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
max_users = models.IntegerField() # maximum number of users per account
|
|
def __str__(self):
|
|
return f"{self.name} - {self.price}"
|
|
# Dealer Model
|
|
class Dealer(models.Model, LocalizedNameMixin):
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer")
|
|
crn = models.CharField(
|
|
max_length=10, verbose_name=_("Commercial Registration Number"),null=True,blank=True
|
|
)
|
|
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"),null=True,blank=True)
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
name = models.CharField(max_length=255, verbose_name=_("English Name"))
|
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
logo = models.ImageField(
|
|
upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")
|
|
)
|
|
parent_dealer = models.ForeignKey(
|
|
"self",
|
|
on_delete=models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Parent Dealer"),
|
|
related_name="sub_dealers",
|
|
)
|
|
dealer_type = models.CharField(
|
|
max_length=255,
|
|
choices=DEALER_TYPES.choices,
|
|
verbose_name=_("Dealer Type"),
|
|
default=DEALER_TYPES.Owner,
|
|
)
|
|
|
|
@property
|
|
def get_active_plan(self):
|
|
try:
|
|
return self.user.subscription_set.filter(is_active=True).first()
|
|
except SubscriptionPlan.DoesNotExist:
|
|
return None
|
|
class Meta:
|
|
verbose_name = _("Dealer")
|
|
verbose_name_plural = _("Dealers")
|
|
permissions = [
|
|
('change_dealer_type', 'Can change dealer type'),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
@property
|
|
def get_sub_dealers(self):
|
|
if self.dealer_type == "Owner":
|
|
return self.sub_dealers.all()
|
|
return None
|
|
|
|
@property
|
|
def is_parent(self):
|
|
return self.dealer_type == "Owner"
|
|
@property
|
|
def get_parent_or_self(self):
|
|
return self.parent_dealer if self.parent_dealer else self
|
|
|
|
# Vendor Model
|
|
class Vendor(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors")
|
|
crn = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("Commercial Registration Number")
|
|
)
|
|
vrn = models.CharField(
|
|
max_length=15, unique=True, verbose_name=_("VAT Registration Number")
|
|
)
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
name = models.CharField(max_length=255, verbose_name=_("English Name"))
|
|
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
|
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
logo = models.ImageField(
|
|
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Vendor")
|
|
verbose_name_plural = _("Vendors")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
# Customer Model
|
|
class Customer(models.Model):
|
|
dealer = models.ForeignKey(
|
|
Dealer, on_delete=models.CASCADE, related_name="customers"
|
|
)
|
|
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
|
|
middle_name = models.CharField(
|
|
max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
|
|
)
|
|
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
|
|
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
|
national_id = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("National ID")
|
|
)
|
|
phone_number = PhoneNumberField(
|
|
region="SA", unique=True, verbose_name=_("Phone Number")
|
|
)
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Customer")
|
|
verbose_name_plural = _("Customers")
|
|
|
|
def __str__(self):
|
|
middle = f" {self.middle_name}" if self.middle_name else ""
|
|
return f"{self.first_name}{middle} {self.last_name}"
|
|
|
|
@property
|
|
def get_full_name(self):
|
|
return f"{self.first_name} {self.middle_name} {self.last_name}"
|
|
|
|
|
|
class Organization(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations')
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
crn = models.CharField(max_length=15, verbose_name=_("Commercial Registration Number"))
|
|
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
|
|
phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number"))
|
|
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
|
|
logo = models.ImageField(upload_to="logos", blank=True, null=True, verbose_name=_("Logo"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Organization")
|
|
verbose_name_plural = _("Organizations")
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Representative(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='representatives')
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
id_number = models.CharField(max_length=10, verbose_name=_("ID Number"))
|
|
phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number"))
|
|
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
|
|
organization = models.ManyToManyField(Organization, related_name='representatives')
|
|
|
|
class Meta:
|
|
verbose_name = _("Representative")
|
|
verbose_name_plural = _("Representatives")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class SaleQuotation(models.Model):
|
|
STATUS_CHOICES = [
|
|
("DRAFT", _("Draft")),
|
|
("CONFIRMED", _("Confirmed")),
|
|
("CANCELED", _("Canceled")),
|
|
]
|
|
dealer = models.ForeignKey(
|
|
Dealer, on_delete=models.CASCADE, related_name="sales", null=True
|
|
)
|
|
customer = models.ForeignKey(
|
|
Customer,
|
|
on_delete=models.CASCADE,
|
|
related_name="quotations",
|
|
verbose_name=_("Customer"),
|
|
)
|
|
amount = models.DecimalField(
|
|
decimal_places=2,
|
|
default=Decimal("0.00"),
|
|
max_digits=10,
|
|
verbose_name=_("Amount"),
|
|
)
|
|
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
|
status = models.CharField(
|
|
max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status")
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
|
|
|
@property
|
|
def total_quantity(self):
|
|
total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total']
|
|
return total_quantity or 0
|
|
|
|
def confirm(self):
|
|
"""Confirm the quotation and lock financial details."""
|
|
if self.status != "DRAFT":
|
|
raise ValueError(_("Only draft quotations can be confirmed."))
|
|
self.status = "CONFIRMED"
|
|
self.save()
|
|
|
|
def cancel(self):
|
|
"""Cancel the quotation."""
|
|
if self.status == "CONFIRMED":
|
|
raise ValueError(_("Cannot cancel a confirmed quotation."))
|
|
self.status = "CANCELED"
|
|
self.save()
|
|
|
|
def __str__(self):
|
|
return f"Quotation #{self.id} for {self.customer}"
|
|
|
|
|
|
class SaleQuotationCar(models.Model):
|
|
quotation = models.ForeignKey(
|
|
SaleQuotation,
|
|
on_delete=models.CASCADE,
|
|
related_name="quotation_cars",
|
|
verbose_name=_("Quotation"),
|
|
)
|
|
car = models.ForeignKey(
|
|
Car,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("Car")
|
|
)
|
|
quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
|
|
|
|
@property
|
|
def financial_details(self):
|
|
"""
|
|
Retrieve financial details dynamically from CarFinance.
|
|
Returns a dictionary with all financial fields for better access.
|
|
"""
|
|
car_finance = self.car.finances
|
|
if not car_finance:
|
|
return None
|
|
|
|
return {
|
|
"selling_price": car_finance.selling_price,
|
|
"administration_fee": car_finance.administration_fee,
|
|
"transportation_fee": car_finance.transportation_fee,
|
|
"custom_card_fee": car_finance.custom_card_fee,
|
|
"registration_fee": car_finance.registration_fee,
|
|
"vat_amount": car_finance.vat_amount,
|
|
# "total_amount": car_finance.total,
|
|
}
|
|
|
|
@property
|
|
def total_price(self):
|
|
"""
|
|
Calculate total price dynamically based on quantity and selling price.
|
|
"""
|
|
car_finance = self.car.finances
|
|
if not car_finance:
|
|
return Decimal("0.00")
|
|
return car_finance.selling_price * self.quantity
|
|
|
|
def __str__(self):
|
|
return f"{self.car} - Quotation #{self.quotation.id}"
|
|
|
|
|
|
class SalesOrder(models.Model):
|
|
quotation = models.OneToOneField(
|
|
SaleQuotation,
|
|
on_delete=models.CASCADE,
|
|
related_name="sales_order",
|
|
verbose_name=_("Quotation"),
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
|
total_amount = models.DecimalField(
|
|
max_digits=14, decimal_places=2, verbose_name=_("Total Amount")
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"Sales Order #{self.id} from Quotation #{self.quotation.id}"
|