haikal/inventory/models.py
Marwan Alwali b8b388262a update
2025-01-06 21:55:48 +03:00

1178 lines
45 KiB
Python

import itertools
from uuid import uuid4
from django.conf import settings
from django.db import models, transaction
from django.db.models import Sum, F, Count
from django.contrib.auth.models import User, UserManager
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 .utilities.financials import get_financial_value, get_total, get_total_financials
from django.db.models import FloatField
from .mixins import LocalizedNameMixin
from django_ledger.models import EntityModel
class DealerUserManager(UserManager):
def create_user_with_dealer(self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields):
user = self.create_user(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 UnitOfMeasure(models.TextChoices):
EACH = 'EA', 'Each'
PAIR = 'PR', 'Pair'
SET = 'SET', 'Set'
GALLON = 'GAL', 'Gallon'
LITER = 'L', 'Liter'
METER = 'M', 'Meter'
KILOGRAM = 'KG', 'Kilogram'
HOUR = 'HR', 'Hour'
BOX = 'BX', 'Box'
ROLL = 'RL', 'Roll'
PACKAGE = 'PKG', 'Package'
DOZEN = 'DZ', 'Dozen'
SQUARE_METER = 'SQ_M', 'Square Meter'
PIECE = 'PC', 'Piece'
BUNDLE = 'BDL', 'Bundle'
class VatRate(models.Model):
rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15'))
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
@property
def vat_rate(self):
return self.rate / 100
def __str__(self):
return f"Rate: {self.rate}%"
class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
is_sa_import = models.BooleanField(default=False)
def __str__(self):
return self.name
class Meta:
verbose_name = "Make"
class CarModel(models.Model, LocalizedNameMixin):
id_car_model = models.AutoField(primary_key=True)
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
name = models.CharField(max_length=255, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Model"
class CarGeneration(models.Model):
id_car_generation = models.AutoField(primary_key=True)
id_car_model = models.ForeignKey(CarModel, models.DO_NOTHING, db_column='id_car_model')
name = models.CharField(max_length=255, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
year_begin = models.CharField(max_length=255, blank=True, null=True)
year_end = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Generation"
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 CarEquipment(models.Model, LocalizedNameMixin):
id_car_equipment = models.AutoField(primary_key=True)
class CarSpecification(models.Model, LocalizedNameMixin):
id_car_specification = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
id_parent = models.ForeignKey("self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Specification"
class CarSpecificationValue(models.Model):
id_car_specification_value = models.AutoField(primary_key=True)
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim")
id_car_specification = models.ForeignKey(CarSpecification, models.DO_NOTHING, db_column="id_car_specification")
value = models.CharField(max_length=500)
unit = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return f"{self.id_car_specification.name}: {self.value} {self.unit}"
class Meta:
verbose_name = "Specification Value"
class 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 AdditionalServices(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
description = models.TextField(verbose_name=_("Description"))
price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Price"))
taxable = models.BooleanField(default=False, verbose_name=_("taxable"))
uom = models.CharField(max_length=10, choices=UnitOfMeasure.choices, verbose_name=_("Unit of Measurement"))
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, verbose_name=_("Dealer"))
class Meta:
verbose_name = _("Additional Services")
verbose_name_plural = _("Additional Services")
def __str__(self):
return self.name + " - " + str(self.price)
class Car(models.Model):
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
dealer = models.ForeignKey(
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
)
vendor = models.ForeignKey(
"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 get_car_group(self):
return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}"
# 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):
additional_services = models.ManyToManyField(ItemModel, 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'))
# 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,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'))
@property
def total(self):
total = 0
if self.additional_services.count() != 0:
total_additional_services = sum(x.default_amount for x in self.additional_services.all())
total = self.selling_price + total_additional_services
else:
total = self.selling_price
if self.discount_amount != 0:
total = total - self.discount_amount
return total
@property
def vat_amount(self):
vat = VatRate.objects.filter(is_active=True).first()
return (self.total * vat.vat_rate).quantize(Decimal('0.01'))
@property
def total_vat(self):
return self.total + self.vat_amount
def __str__(self):
return f"Car: {self.car}, Selling Price: {self.selling_price}"
# def save(self, *args, **kwargs):
# self.full_clean()
# try:
# price_after_discount = self.selling_price - self.discount_amount
# self.profit_margin = price_after_discount - self.cost_price
# self.vat_amount = settings.VAT_RATE
# except InvalidOperation as e:
# raise ValidationError(_("Invalid decimal operation: %s") % str(e))
# super().save(*args, **kwargs)
class Meta:
verbose_name = _("Car Financial Details")
verbose_name_plural = _("Car Financial Details")
class ExteriorColors(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
class Meta:
verbose_name = _("Exterior Colors")
verbose_name_plural = _("Exterior Colors")
def __str__(self):
return f"{self.name} ({self.rgb})"
class InteriorColors(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
class Meta:
verbose_name = _("Interior Colors")
verbose_name_plural = _("Interior Colors")
def __str__(self):
return f"{self.name} ({self.rgb})"
class CarColors(models.Model):
car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors")
exterior = models.ForeignKey(
"ExteriorColors", on_delete=models.CASCADE, related_name="colors"
)
interior = models.ForeignKey(
"InteriorColors", on_delete=models.CASCADE, related_name="colors"
)
class Meta:
verbose_name = _("Color")
verbose_name_plural = _("Colors")
unique_together = ("car", "exterior", "interior")
def __str__(self):
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
class CustomCard(models.Model):
car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car"))
custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number"))
custom_date = models.DateField(verbose_name=_("Custom Date"))
class Meta:
verbose_name = _("Custom Card")
verbose_name_plural = _("Custom Cards")
def __str__(self):
return f"{self.car} - {self.custom_number}"
class CarLocation(models.Model):
car = models.OneToOneField(
Car,
on_delete=models.CASCADE,
related_name='location',
verbose_name=_("Car")
)
owner = models.ForeignKey(
'Dealer',
on_delete=models.CASCADE,
related_name='owned_cars',
verbose_name=_("Owner"),
help_text=_("Dealer who owns the car.")
)
showroom = models.ForeignKey(
'Dealer',
on_delete=models.CASCADE,
related_name='showroom_cars',
verbose_name=_("Showroom"),
help_text=_("Dealer where the car is displayed (can be the owner).")
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_("Description"),
help_text=_("Optional description about the showroom placement.")
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Created At")
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name=_("Last Updated")
)
class Meta:
verbose_name = _("Car Location")
verbose_name_plural = _("Car Locations")
def __str__(self):
return f"Car: {self.car}, Showroom: {self.showroom}, Owner: {self.owner}"
def is_owner_showroom(self):
"""
Returns True if the showroom is the same as the owner.
"""
return self.owner == self.showroom
class CarRegistration(models.Model):
car = models.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
class Subscription(models.Model):
plan = models.ForeignKey("SubscriptionPlan", on_delete=models.CASCADE, related_name="subscriptions")
start_date = models.DateField(help_text="Date when the subscription starts")
end_date = models.DateField(help_text="Date when the subscription ends")
users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model
is_active = models.BooleanField(default=True)
billing_cycle = models.CharField(
max_length=10,
choices=[('monthly', 'Monthly'), ('annual', 'Annual')],
default='monthly',
help_text="Billing cycle for the subscription"
)
last_payment_date = models.DateField(null=True, blank=True, help_text="Date of the last payment made")
next_payment_date = models.DateField(null=True, blank=True, help_text="Date of the next payment due")
class Meta:
verbose_name = _("Subscription")
verbose_name_plural = _("Subscriptions")
def __str__(self):
return self.plan.name
@property
def total_subscribers(self):
return self.users.count()
class SubscriptionUser(models.Model):
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
verbose_name = _("Subscription User")
verbose_name_plural = _("Subscription Users")
def __str__(self):
return f"{self.subscription} - {self.user}"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=100, unique=True, help_text=_("Name of the subscription plan"))
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
max_users = models.PositiveIntegerField(help_text=_("Maximum number of users allowed"), default=1)
max_inventory_size = models.PositiveIntegerField(help_text=_("Maximum number of cars in inventory"), default=50)
support_level = models.CharField(
max_length=50,
choices=[('basic', 'Basic Support'), ('priority', 'Priority Support'), ('dedicated', 'Dedicated Support')],
default='basic',
help_text="Level of support provided"
)
custom_features = models.JSONField(blank=True, null=True, help_text=_("Additional features specific to this plan"))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("Subscription Plan")
verbose_name_plural = _("Subscription Plans")
def __str__(self):
return f"{self.name} - {self.price}"
class Dealer(models.Model, LocalizedNameMixin):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer")
crn = models.CharField(max_length=10,
verbose_name=_("Commercial Registration Number")
,null=True
,blank=True)
vrn = models.CharField(max_length=15,
verbose_name=_("VAT Registration Number"),
null=True,
blank=True)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField(max_length=200,
blank=True,
null=True,
verbose_name=_("Address"))
logo = models.ImageField(upload_to="logos/users",
blank=True,
null=True,
verbose_name=_("Logo"))
entity = models.ForeignKey(EntityModel, on_delete=models.SET_NULL, null=True, blank=True)
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
objects = DealerUserManager()
@property
def get_active_plan(self):
try:
return self.user.subscription_set.filter(is_active=True).first()
except SubscriptionPlan.DoesNotExist:
return None
@property
def get_plan(self):
active_plan = self.get_active_plan
if active_plan:
subscription_plan = SubscriptionPlan.objects.filter(
pk=active_plan.pk
).first()
if subscription_plan:
return subscription_plan
return None
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
# permissions = [
# ('change_dealer_type', 'Can change dealer type'),
# ]
def __str__(self):
return self.name
# @property
# def get_sub_dealers(self):
# if self.dealer_type == "OWNER":
# return self.sub_dealers.all()
# return None
#
# @property
# def is_parent(self):
# return self.dealer_type == "OWNER"
# @property
# def get_root_dealer(self):
# return self.parent_dealer if self.parent_dealer else self
##############################
# Additional staff types for later
# COORDINATOR = "coordinator", _("Coordinator")
# RECEPTIONIST = "receptionist", _("Receptionist")
# AGENT = "agent", _("Agent")
# TECHNICIAN = "technician", _("Technician")
# DRIVER = "driver", _("Driver")
##############################
class StaffTypes(models.TextChoices):
MANAGER = "manager", _("Manager")
INVENTORY = "inventory", _("Inventory")
ACCOUNTANT = "accountant", _("Accountant")
SALES = "sales", _("Sales")
COORDINATOR = "coordinator", _("Coordinator")
RECEPTIONIST = "receptionist", _("Receptionist")
AGENT = "agent", _("Agent")
class Staff(models.Model, LocalizedNameMixin):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
staff_type = models.CharField(choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
class Meta:
verbose_name = _("Staff")
verbose_name_plural = _("Staff")
permissions = []
def __str__(self):
return f"{self.name} - {self.get_staff_type_display()}"
class ActionChoices(models.TextChoices):
CREATE = "create", _("Create")
UPDATE = "update", _("Update")
DELETE = "delete", _("Delete")
STATUS_CHANGE = "status_change", _("Status Change")
class DealStatus(models.TextChoices):
NEW = "new", _("New")
PENDING = "pending", _("Pending")
CANCELED = "canceled", _("Canceled")
COMPLETED = "completed", _("Completed")
class Priority(models.TextChoices):
LOW = "low", _("Low")
MEDIUM = "medium", _("Medium")
HIGH = "high", _("High")
class Sources(models.TextChoices):
REFERRALS = "referrals", _("Referrals")
WALK_IN = "walk_in", _("Walk In")
TOLL_FREE = "toll_free", _("Toll Free")
WHATSAPP = "whatsapp", _("WhatsApp")
SHOWROOM = "showroom", _("Showroom")
WEBSITE = "website", _("Website")
TIKTOK = "tiktok", _("TikTok")
INSTAGRAM = "instagram", _("Instagram")
X = "x", _("X")
FACEBOOK = "facebook", _("Facebook")
MOTORY = "motory", _("Motory")
INFLUENCERS = "influencers", _("Influencers")
YOUTUBE = "youtube", _("Youtube")
EMAIL = "email", _("Email")
class ContactStatus(models.TextChoices):
NEW = "new", _("New")
PENDING = "pending", _("Pending")
ASSIGNED = "assigned", _("Assigned")
CONTACTED = "contacted", _("Contacted")
ACCEPTED = "accepted", _("Accepted")
QUALIFIED = "qualified", _("Qualified")
CANCELED = "canceled", _("Canceled")
# class Contact(models.Model):
# AGE_RANGES = (
# ('18-30', '18 - 30'),
# ('31-40', '31 - 40'),
# ('41-50', '41 - 50'),
# ('51-60', '51 - 60'),
# ('61-70', '61 - 70'),
# ('71-80', '71 - 80'),
# ('81-90', '81 - 90'),
# )
#
# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="contacts")
# first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
# last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
# age = models.CharField(choices=AGE_RANGES, max_length=20, verbose_name=_("Age"))
# gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender"))
# phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
# email = models.EmailField(verbose_name=_("Email"))
# id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, verbose_name=_("Make"))
# id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, verbose_name=_("Model"))
# year = models.PositiveSmallIntegerField(verbose_name=_("Year"))
# status = models.CharField(choices=ContactStatus.choices, max_length=255, verbose_name=_("Status"), default=ContactStatus.NEW)
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
# updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
# enquiry_type = models.CharField(choices=[("quotation", _("Quote")),("testdrive", _("Test drive"))], max_length=50, verbose_name=_("Enquiry Type"))
# purchase_method = models.CharField(choices=[("c", _("Cash")),("f", _("Finance"))], max_length=1, verbose_name=_("Purchase Method"))
# source = models.CharField(max_length=100, choices=Sources.choices, verbose_name=_("Source"))
# salary = models.PositiveIntegerField(verbose_name=_("Salary"))
# obligations = models.PositiveIntegerField(verbose_name=_("Obligations"))
#
# class Meta:
# verbose_name = _("Contact")
# verbose_name_plural = _("Contacts")
#
# def __str__(self):
# return self.first_name + " " + self.last_name
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"))
is_lead = models.BooleanField(default=False, verbose_name=_("Is Lead"))
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 Opportunity(models.Model):
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"))
deal_name = models.CharField(max_length=255, verbose_name=_("Deal Name"))
deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value"))
deal_status = models.CharField(max_length=20, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status"))
priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority"))
created_by = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="deals_created")
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
class Meta:
verbose_name = _("Opportunity")
verbose_name_plural = _("Opportunities")
def __str__(self):
return f"{self.deal_name} - {self.customer.get_full_name}"
class Notes(models.Model):
opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="notes")
note = models.TextField(verbose_name=_("Note"))
created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="notes_created")
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
class Meta:
verbose_name = _("Notes")
verbose_name_plural = _("Notes")
class OpportunityLog(models.Model):
opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="logs")
action = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Action"))
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, verbose_name=_("Staff"))
old_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("Old Status"))
new_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("New Status"))
details = models.TextField(blank=True, null=True, verbose_name=_("Details"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
class Meta:
verbose_name = _("Log")
verbose_name_plural = _("Logs")
ordering = ['-created_at']
def __str__(self):
return f"{self.get_action_display()} by {self.user} on {self.opportunity.deal_name}"
class Notification(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
message = models.CharField(max_length=255, verbose_name=_("Message"))
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
class Meta:
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
ordering = ['-created_at']
def __str__(self):
return self.message
class Vendor(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors")
crn = models.CharField(
max_length=10, unique=True, verbose_name=_("Commercial Registration Number")
)
vrn = models.CharField(
max_length=15, unique=True, verbose_name=_("VAT Registration Number")
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")
def __str__(self):
return self.name
class Organization(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations')
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
crn = models.CharField(max_length=15, verbose_name=_("Commercial Registration Number"))
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number"))
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
logo = models.ImageField(upload_to="logos", blank=True, null=True, verbose_name=_("Logo"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
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"))
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
organization = models.ManyToManyField(Organization, related_name='representatives')
class Meta:
verbose_name = _("Representative")
verbose_name_plural = _("Representatives")
def __str__(self):
return self.name
class SaleQuotation(models.Model):
quotation_number = models.CharField(max_length=10, unique=True)
STATUS_CHOICES = [
("Draft", _("Draft")),
("Approved", _("Approved")),
("In Review", _("In Review")),
("Paid", _("Paid")),
]
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sales", null=True
)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name="quotations",
verbose_name=_("Customer"),
)
amount = models.DecimalField(
decimal_places=2,
default=Decimal("0.00"),
max_digits=10,
verbose_name=_("Amount"),
)
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
is_approved = models.BooleanField(default=False)
status = models.CharField(
max_length=10, choices=STATUS_CHOICES, default="Draft", verbose_name=_("Status")
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
posted = models.BooleanField(default=False)
payment_id = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Payment ID"))
is_paid = models.BooleanField(default=False)
date_draft = models.DateTimeField(null=True, blank=True, verbose_name=_('Draft Date'))
date_in_review = models.DateTimeField(null=True, blank=True, verbose_name=_('In Review Date'))
date_approved = models.DateTimeField(null=True, blank=True, verbose_name=_('Approved Date'))
date_paid = models.DateTimeField(null=True, blank=True, verbose_name=_('Paid Date'))
date_void = models.DateTimeField(null=True, blank=True, verbose_name=_('Void Date'))
date_canceled = models.DateTimeField(null=True, blank=True, verbose_name=_('Canceled Date'))
@property
def total_quantity(self):
total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total']
return total_quantity or 0
@property
def total(self):
total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity')))
if not total:
return 0
return total["total_price"]
@property
def total_vat(self):
if self.total:
return float(self.total) * 0.15 + float(self.total)
return 0
# def confirm(self):
# """Confirm the quotation and lock financial details."""
# if self.status != "DRAFT":
# raise ValueError(_("Only draft quotations can be confirmed."))
# self.status = "CONFIRMED"
# self.save()
# def cancel(self):
# """Cancel the quotation."""
# if self.status == "CONFIRMED":
# raise ValueError(_("Cannot cancel a confirmed quotation."))
# self.status = "CANCELED"
# self.save()
def __str__(self):
return f"Quotation #{self.quotation_number} for {self.customer}"
@property
def display_quotation_number(self):
return f"QN-{self.quotation_number}"
def save(self, *args, **kwargs):
if not self.quotation_number:
self.quotation_number = str(next(self._get_quotation_number())).zfill(6)
super().save(*args, **kwargs)
@classmethod
def _get_quotation_number(cls):
last_quotation = cls.objects.all().order_by('id').last()
if last_quotation:
last_quotation_number = int(last_quotation.quotation_number)
else:
last_quotation_number = 0
return itertools.count(last_quotation_number + 1)
class SaleQuotationCar(models.Model):
quotation = models.ForeignKey(
SaleQuotation,
on_delete=models.CASCADE,
related_name="quotation_cars",
verbose_name=_("Quotation"),
)
car = models.ForeignKey(
Car,
on_delete=models.CASCADE,
verbose_name=_("Car")
)
quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
@property
def finance(self):
return self.car.finances
@property
def financial_details(self):
"""
Retrieve financial details dynamically from CarFinance.
Returns a dictionary with all financial fields for better access.
"""
car_finance = self.car.finances
if not car_finance:
return None
return {
"selling_price": car_finance.selling_price,
"administration_fee": car_finance.administration_fee,
"transportation_fee": car_finance.transportation_fee,
"custom_card_fee": car_finance.custom_card_fee,
"registration_fee": car_finance.registration_fee,
"vat_amount": car_finance.vat_amount,
# "total_amount": car_finance.total,
}
@property
def total(self):
"""
Calculate total price dynamically based on quantity and selling price.
"""
if not self.car.finances:
return Decimal("0.00")
return self.car.finances.selling_price * self.quantity
@property
def total_vat(self):
"""
Calculate total price dynamically based on quantity and selling price.
"""
if not self.car.finances:
return Decimal("0.00")
price = float(self.car.finances.selling_price * self.quantity)
return (price * 0.15) + price
def __str__(self):
return f"{self.car} - Quotation #{self.quotation.id}"
class SalesOrder(models.Model):
quotation = models.OneToOneField(
SaleQuotation,
on_delete=models.CASCADE,
related_name="sales_order",
verbose_name=_("Quotation"),
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
total_amount = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Total Amount")
)
def __str__(self):
return f"Sales Order #{self.id} from Quotation #{self.quotation.id}"
class Payment(models.Model):
METHOD_CHOICES = [
('cash', _('cash')),
('credit', _('credit')),
('transfer', _('transfer')),
('debit', _('debit')),
('SADAD', _('SADAD')),
]
quotation = models.ForeignKey(SaleQuotation, on_delete=models.CASCADE, related_name="payments")
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("amount"))
payment_method = models.CharField(choices=METHOD_CHOICES, max_length=50, verbose_name=_("method"))
reference_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("reference number"))
payment_date = models.DateField(auto_now_add=True, verbose_name=_("date"))
# def save(self, *args, **kwargs):
# super().save(*args, **kwargs)
# self.quotation.remaining_balance -= self.amount
# if self.quotation.remaining_balance <= 0:
# self.quotation.is_paid = True
# self.quotation.save()
class Meta:
verbose_name = _("payment")
verbose_name_plural = _("payments")
def __str__(self):
return f"Payment of {self.amount} on {self.payment_date} for {self.quotation}"
class Refund(models.Model):
payment = models.OneToOneField(Payment, on_delete=models.CASCADE, related_name="refund")
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("amount"))
reason = models.TextField(blank=True, verbose_name=_("reason"))
refund_date = models.DateField(auto_now_add=True, verbose_name=_("refund date"))
class Meta:
verbose_name = _("refund")
verbose_name_plural = _("refunds")
def __str__(self):
return f"Refund of {self.amount} on {self.refund_date}"
class UserActivityLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "User Activity Log"
verbose_name_plural = "User Activity Logs"
ordering = ['-timestamp']
def __str__(self):
return f"{self.user.email} - {self.action} - {self.timestamp}"