haikal/inventory/models.py

3964 lines
130 KiB
Python

import logging
import uuid
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import Permission
from inventory.validators import SaudiPhoneNumberValidator
from decimal import Decimal
from django.urls import reverse
from django.utils.text import slugify
from django.utils import timezone
from django.core.validators import MinValueValidator,MaxValueValidator
import hashlib
from django.db import models
from datetime import timedelta
from django.contrib.auth.models import User, UserManager
from django.utils.translation import gettext_lazy as _
from django_ledger.models import (
VendorModel,
EntityModel,
ItemModel,
CustomerModel,
JournalEntryModel,
LedgerModel,
)
from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ValidationError
from phonenumber_field.modelfields import PhoneNumberField
from django.utils.timezone import now
from django.contrib.auth.models import Group
from inventory.utils import get_user_type, make_random_password, to_dict
from .mixins import LocalizedNameMixin
from django_ledger.models import (
EstimateModel,
InvoiceModel,
AccountModel,
EntityManagementModel,
PurchaseOrderModel,
ItemTransactionModel,
BillModel,
)
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder
# from appointment.models import StaffMember
from plans.quota import get_user_quota
from plans.models import UserPlan
from django.db.models import Q
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
from encrypted_model_fields.fields import (
EncryptedCharField,
EncryptedDateField,
EncryptedEmailField,
)
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
from plans.models import Invoice
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Base(models.Model):
id = models.UUIDField(
unique=True,
editable=False,
default=uuid.uuid4,
primary_key=True,
verbose_name=_("Primary Key"),
)
slug = models.SlugField(
null=True,
blank=True,
unique=True,
verbose_name=_("Slug"),
help_text=_(
"Slug for the object. If not provided, it will be generated automatically."
),
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def clean(self):
if isinstance(self.id, str):
try:
uuid.UUID(self.id)
except ValueError:
raise ValidationError({"id": "Invalid UUID format"})
super().clean()
class Meta:
abstract = True
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,
)
return user
class DealersMake(models.Model):
"""
Represents the relationship between a car dealer and a car make.
This model establishes a many-to-many relationship between dealers and
car makes, allowing each dealer to be associated with multiple car makes
and each car make to be associated with multiple dealers. It also keeps
track of the date and time when the relationship was added.
:ivar dealer: The dealer associated with the car make.
:type dealer: ForeignKey
:ivar car_make: The car make associated with the dealer.
:type car_make: ForeignKey
:ivar added_at: The date and time when the relationship was created.
:type added_at: DateTimeField
"""
dealer = models.ForeignKey(
"Dealer", on_delete=models.CASCADE, related_name="dealer_makes"
)
car_make = models.ForeignKey(
"CarMake", on_delete=models.CASCADE, related_name="car_dealers"
)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("dealer", "car_make") # Prevents duplicate entries
def __str__(self):
return f"{self.dealer.name} - {self.car_make.name}"
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,
)
return user
class EmailStatus(models.TextChoices):
SENT = "SENT", "Sent"
FAILED = "FAILED", "Failed"
DELIVERED = "DELIVERED", "Delivered"
OPEN = "OPEN", "Open"
DRAFT = "DRAFT", "Draft"
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):
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE)
rate = models.DecimalField(
max_digits=5,
decimal_places=2,
default=Decimal("0.15"),
validators=[
MinValueValidator(0.0),
MaxValueValidator(1.0)
],
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)")
)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Rate: {self.rate * 100}%"
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class CarType(models.IntegerChoices):
CAR = 1, _("Car")
LIGHT_COMMERCIAL = 2, _("Light Commercial")
HEAVY_DUTY_TRACTORS = 3, _("Heavy-Duty Tractors")
TRAILERS = 4, _("Trailers")
MEDIUM_TRUCKS = 5, _("Medium Trucks")
BUSES = 6, _("Buses")
MOTORCYCLES = 20, _("Motorcycles")
BUGGY = 21, _("Buggy")
MOTO_ATV = 22, _("Moto ATV")
SCOOTERS = 23, _("Scooters")
KARTING = 24, _("Karting")
ATV = 25, _("ATV")
SNOWMOBILES = 26, _("Snowmobiles")
class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, 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, default="user-logo.jpg"
)
is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Make")
indexes = [
models.Index(fields=["name"], name="car_make_name_idx"),
models.Index(fields=["is_sa_import"], name="car_make_sa_import_idx"),
models.Index(fields=["car_type"], name="car_make_type_idx"),
]
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)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Model")
indexes = [
models.Index(fields=["id_car_make"], name="car_model_make_idx"),
models.Index(fields=["name"], name="car_model_name_idx"),
models.Index(
fields=["id_car_make", "name"], name="car_model_make_name_idx"
),
]
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, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
year_begin = models.IntegerField(blank=True, null=True)
year_end = models.IntegerField(blank=True, null=True)
generation_name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Series")
indexes = [
models.Index(fields=["id_car_model"], name="car_serie_model_idx"),
models.Index(fields=["year_begin", "year_end"], name="car_serie_years_idx"),
models.Index(fields=["name"], name="car_serie_name_idx"),
models.Index(fields=["generation_name"], name="car_serie_generation_idx"),
]
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, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
start_production_year = models.IntegerField(blank=True, null=True)
end_production_year = models.IntegerField(blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Trim")
indexes = [
models.Index(fields=["id_car_serie"], name="car_trim_serie_idx"),
models.Index(
fields=["start_production_year", "end_production_year"],
name="car_trim_prod_years_idx",
),
models.Index(fields=["name"], name="car_trim_name_idx"),
]
class CarEquipment(models.Model, LocalizedNameMixin):
id_car_equipment = models.AutoField(primary_key=True)
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim")
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)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Equipment")
indexes = [
models.Index(fields=["id_car_trim"], name="car_equipment_trim_idx"),
models.Index(fields=["year_begin"], name="car_equipment_year_idx"),
models.Index(fields=["name"], name="car_equipment_name_idx"),
]
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
)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Specification")
indexes = [
models.Index(fields=["id_parent"], name="car_spec_parent_idx"),
models.Index(fields=["name"], name="car_spec_name_idx"),
]
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")
indexes = [
models.Index(fields=["id_car_trim"], name="car_spec_val_trim_idx"),
models.Index(fields=["id_car_specification"], name="car_spec_val_spec_idx"),
models.Index(
fields=["id_car_trim", "id_car_specification"],
name="car_spec_val_trim_spec_idx",
),
]
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
)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Option")
indexes = [
models.Index(fields=["id_parent"], name="car_option_parent_idx"),
models.Index(fields=["name"], name="car_option_name_idx"),
]
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"
)
value = models.CharField(max_length=500)
unit = models.CharField(max_length=255, blank=True, null=True)
is_base = models.IntegerField()
def __str__(self):
return f"{self.id_car_option.name}: {self.value} {self.unit}"
class Meta:
verbose_name = _("Option Value")
indexes = [
models.Index(fields=["id_car_option"], name="car_opt_val_option_idx"),
models.Index(fields=["id_car_equipment"], name="car_opt_val_equipment_idx"),
models.Index(fields=["is_base"], name="car_opt_val_is_base_idx"),
models.Index(
fields=["id_car_option", "id_car_equipment"],
name="cov_option_equipment_idx",
),
]
class CarTransferStatusChoices(models.TextChoices):
draft = "draft", _("Draft")
approved = "approved", _("Approved")
pending = "pending", _("Pending")
accepted = "accepted", _("Accepted")
success = "success", _("Success")
reject = "reject", _("Reject")
cancelled = "cancelled", _("Cancelled")
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")
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")
)
item = models.OneToOneField(
ItemModel,
on_delete=models.CASCADE,
verbose_name=_("Item"),
null=True,
blank=True,
)
def to_dict(self):
return {
"name": self.name,
"price": str(self.price),
"price_": str(self.price_),
"taxable": self.taxable,
"uom": self.uom,
"service_tax": str(self.service_tax),
}
@property
def price_(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return (
Decimal(self.price + (self.price * vat.rate))
if self.taxable
else self.price
)
@property
def service_tax(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return Decimal(self.price * vat.rate)
class Meta:
verbose_name = _("Additional Services")
verbose_name_plural = _("Additional Services")
def __str__(self):
return self.name + " - " + str(self.price_)
class Car(Base):
item_model = models.OneToOneField(
ItemModel,
models.DO_NOTHING,
verbose_name=_("Item Model"),
null=True,
blank=True,
)
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,
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"),
)
#
additional_services = models.ManyToManyField(
AdditionalServices, related_name="additionals"
)
cost_price = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Cost Price"),
default=Decimal("0.00"),
)
selling_price = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Selling Price"),
default=Decimal("0.00"),
)
marked_price = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Marked Price"),
default=Decimal("0.00"),
)
discount_amount = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Discount Amount"),
default=Decimal("0.00"),
)
#
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"))
sold_date = models.DateTimeField(verbose_name=_("Sold Date"), null=True, blank=True)
hash = models.CharField(
max_length=64, blank=True, null=True, verbose_name=_("Hash")
)
# history = HistoricalRecords()
def get_absolute_url(self):
return reverse(
"car_detail", kwargs={"dealer_slug": self.dealer.slug, "slug": self.slug}
)
def save(self, *args, **kwargs):
self.slug = slugify(self.vin)
self.hash = self.get_hash
super(Car, self).save(*args, **kwargs)
class Meta:
verbose_name = _("Car")
verbose_name_plural = _("Cars")
indexes = [
models.Index(fields=["vin"], name="car_vin_idx"),
models.Index(fields=["year"], name="car_year_idx"),
models.Index(fields=["status"], name="car_status_idx"),
models.Index(fields=["dealer"], name="car_dealer_idx"),
models.Index(fields=["vendor"], name="car_vendor_idx"),
models.Index(fields=["id_car_make"], name="car_make_idx"),
models.Index(fields=["id_car_model"], name="car_model_idx"),
models.Index(fields=["id_car_serie"], name="car_serie_idx"),
models.Index(fields=["id_car_trim"], name="car_trim_idx"),
models.Index(
fields=["id_car_make", "id_car_model"], name="car_make_model_idx"
),
models.Index(fields=["id_car_make", "year"], name="car_make_year_idx"),
models.Index(fields=["dealer", "status"], name="car_dealer_status_idx"),
models.Index(fields=["vendor", "status"], name="car_vendor_status_idx"),
models.Index(fields=["year", "status"], name="car_year_status_idx"),
models.Index(
fields=["status"],
name="car_active_status_idx",
condition=Q(status=CarStatusChoices.AVAILABLE),
),
]
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"
vin=self.vin if self.vin else None
return f"{self.year} - {make} - {model} - {trim}-{vin}"
@property
def product(self):
return self.dealer.entity.get_items_all().filter(name=self.vin).first()
def get_reservation(self):
return self.reservations.filter(reserved_until__gt=now()).first()
def is_reserved(self):
active_reservations = self.reservations.filter(reserved_until__gt=now())
return active_reservations.exists()
@property
def logo(self):
return getattr(self.id_car_make, "logo", "")
# @property
# def additional_services(self):
# return self.additional_services.all()
@property
def ready(self):
try:
return all(
[
self.colors,
self.marked_price > 0,
]
)
except Exception:
return False
@property
def invoice(self):
return (
self.item_model.invoicemodel_set.first
if self.item_model.invoicemodel_set.first()
else None
)
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}"
@property
def get_hash(self):
hash_object = hashlib.sha256()
color = ""
try:
color = self.colors.exterior.name if self.colors else ""
except Exception as e:
logger.error(f"Error getting color for car {self.vin} error: {e}")
make = self.id_car_make.name if self.id_car_make else ""
model = self.id_car_model.name if self.id_car_model else ""
year = self.year if self.year else 0
serie = self.id_car_serie.name if self.id_car_serie else ""
trim = self.id_car_trim.name if self.id_car_trim else ""
hash_object.update(f"{make}{model}{year}{serie}{trim}{color}".encode("utf-8"))
return hash_object.hexdigest()
def mark_as_sold(self):
self.cancel_reservation()
self.status = CarStatusChoices.SOLD
self.sold_date=timezone.now()
self.save()
def cancel_reservation(self):
if self.reservations.exists():
self.reservations.all().delete()
def cancel_transfer(self):
if self.transfer_logs.exists():
self.transfer_logs.all().delete()
def to_dict(self):
return {
"vin": self.vin,
"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",
"year": self.year,
"display_name": self.get_car_group,
"status": self.status,
"stock_type": self.stock_type,
"remarks": self.remarks,
"mileage": self.mileage,
"receiving_date": self.receiving_date.strftime("%Y-%m-%d %H:%M:%S"),
"hash": self.get_hash,
"id": str(self.id),
}
def get_specifications(self):
specs = CarSpecificationValue.objects.filter(id_car_trim=self.id_car_trim)
return specs
def get_inventory_account(self):
return (
self.dealer.entity.get_all_accounts()
.filter(name=f"Inventory:{self.id_car_make.name}")
.first()
)
def get_revenue_account(self):
return (
self.dealer.entity.get_all_accounts()
.filter(name=f"Revenue:{self.id_car_make.name}")
.first()
)
def get_cogs_account(self):
return (
self.dealer.entity.get_all_accounts()
.filter(name=f"Cogs:{self.id_car_make.name}")
.first()
)
def add_colors(self, exterior, interior):
self.colors = CarColors.objects.create(
car=self, exterior=exterior, interior=interior
)
self.save()
@property
def logo(self):
return self.id_car_make.logo.url if self.id_car_make.logo else None
#
@property
def get_additional_services_amount(self):
return sum([Decimal(x.price) for x in self.additional_services.all()])
@property
def get_additional_services_amount_(self):
return sum([Decimal(x.price_) for x in self.additional_services.all()])
@property
def get_additional_services_vat(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return sum(
[
Decimal((x.price) * (vat.rate))
for x in self.additional_services.filter(taxable=True)
]
)
def get_additional_services(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return {
"services": [
[x, ((x.price) * (vat.rate) if x.taxable else 0)]
for x in self.additional_services.all()
],
"total_": self.get_additional_services_amount_,
"total": self.get_additional_services_amount,
"services_vat": self.get_additional_services_vat,
}
@property
def final_price(self):
return Decimal(self.marked_price - self.discount)
@property
def vat_amount(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return Decimal(self.final_price) * (vat.rate)
@property
def total_services_and_car_vat(self):
return self.vat_amount + self.get_additional_services()["services_vat"]
@property
def final_price_plus_vat(self):
return Decimal(self.final_price) + Decimal(self.vat_amount)
@property
def final_price_plus_services_plus_vat(self):
return Decimal(self.final_price_plus_vat) + Decimal(
self.get_additional_services()["total_"]
) # total services with vat and car_sell price with vat
# to be used after invoice is created
@property
def invoice(self):
return self.item_model.invoicemodel_set.first() or None
@property
def estimate(self):
return getattr(self.invoice, "ce_model", None)
@property
def discount(self):
if not self.estimate:
return 0
try:
instance = ExtraInfo.objects.get(
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=self.estimate.pk,
)
return Decimal(instance.data.get("discount", 0))
except ExtraInfo.DoesNotExist:
return Decimal(0)
# def get_discount_amount(self,estimate,user):
# try:
# instance = models.ExtraInfo.objects.get(
# dealer=self.dealer,
# content_object=estimate,
# related_object=user
# )
# if instance:
# return instance.data.get("discount",0)
# return 0
# except Exception:
# print("Error getting discount amount")
# return 0
# @property
# def total_discount(self):
# if self.discount_amount > 0:
# return self.marked_price - self.discount_amount
# return self.marked_price
# @property
# def total_vat(self):
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
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.total_vat # TODO : check later
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"),
)
# reserved_for = models.ForeignKey(
# CustomerModel,
# on_delete=models.CASCADE,
# related_name="reservations",
# verbose_name=_("Reserved For"),
# )
reserved_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Reserved At"))
reserved_until = models.DateTimeField(verbose_name=_("Reserved Until"))
@property
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(
# 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"),
# default=Decimal("0.00"),
# )
# marked_price = models.DecimalField(
# max_digits=14,
# decimal_places=2,
# verbose_name=_("Marked Price"),
# default=Decimal("0.00"),
# )
# discount_amount = models.DecimalField(
# max_digits=14,
# decimal_places=2,
# verbose_name=_("Discount Amount"),
# default=Decimal("0.00"),
# )
# # is_sold = models.BooleanField(default=False)
# @property
# def total(self):
# return self.marked_price
# @property
# def total_additionals_no_vat(self):
# return sum(x.price for x in self.additional_services.all())
# @property
# def total_additionals(self):
# return sum(x.price_ for x in self.additional_services.all())
# @property
# def total_discount(self):
# if self.discount_amount > 0:
# return self.marked_price - self.discount_amount
# return self.marked_price
# @property
# def total_vat(self):
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
# @property
# def vat_amount(self):
# vat = VatRate.objects.filter(dealer=self.car.dealer, is_active=True).first()
# if vat:
# return (self.total_discount * Decimal(vat.rate)).quantize(Decimal("0.01"))
# return Decimal("0.00")
# @property
# def revenue(self):
# return self.marked_price - self.cost_price
# def to_dict(self):
# return {
# "cost_price": str(self.cost_price),
# "selling_price": str(self.selling_price),
# "marked_price": str(self.marked_price),
# "discount_amount": str(self.discount_amount),
# "total": str(self.total),
# "total_discount": str(self.total_discount),
# "total_vat": str(self.total_vat),
# "vat_amount": str(self.vat_amount),
# }
# def __str__(self):
# return f"Car: {self.car}, Marked Price: {self.marked_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")
# indexes = [
# models.Index(fields=["car"], name="car_finance_car_idx"),
# models.Index(fields=["cost_price"], name="car_finance_cost_price_idx"),
# models.Index(
# fields=["selling_price"], name="car_finance_selling_price_idx"
# ),
# models.Index(fields=["marked_price"], name="car_finance_marked_price_idx"),
# models.Index(fields=["discount_amount"], name="car_finance_discount_idx"),
# ]
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")
indexes = [
models.Index(fields=["name"], name="exterior_color_name_idx"),
models.Index(fields=["arabic_name"], name="exterior_color_arabic_name_idx"),
]
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")
indexes = [
models.Index(fields=["name"], name="interior_color_name_idx"),
models.Index(fields=["arabic_name"], name="interior_color_arabic_name_idx"),
]
def __str__(self):
return f"{self.name} ({self.rgb})"
class CarColors(models.Model):
car = models.OneToOneField("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")
indexes = [
models.Index(fields=["exterior"], name="car_colors_exterior_idx"),
models.Index(fields=["interior"], name="car_colors_interior_idx"),
models.Index(
fields=["exterior", "interior"], name="car_colors_ext_int_combo_idx"
),
]
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.OneToOneField(
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"), null=True, blank=True
)
text3 = models.CharField(
max_length=1, verbose_name=_("Text 3"), null=True, blank=True
)
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}"
# 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 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 = EncryptedCharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
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"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
entity = models.ForeignKey(
EntityModel, on_delete=models.SET_NULL, null=True, blank=True,related_name="dealers"
)
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
objects = DealerUserManager()
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
@property
def active_plan(self):
try:
plan = UserPlan.objects.get(user=self.user, active=True).plan
return plan
except Exception as e:
print(e)
return None
@property
def is_plan_expired(self):
try:
return UserPlan.objects.get(user=self.user, active=True).is_expired()
except Exception as e:
logger.error(e)
return True
@property
def customers(self):
return models.Customer.objects.filter(dealer=self)
@property
def user_quota(self):
try:
quota_dict = get_user_quota(self.user)
allowed_users = quota_dict.get("Users", None)
return allowed_users
except Exception as e:
print(e)
return None
@property
def car_quota(self):
try:
quota_dict = get_user_quota(self.user)
allowed_cars = quota_dict.get("Cars", None)
return allowed_cars
except Exception as e:
print(e)
return None
def get_vendors(self):
return VendorModel.objects.filter(entity_model=self.entity)
def get_staff(self):
return Staff.objects.filter(dealer=self)
@property
def is_staff_exceed_quota_limit(self):
quota = self.user_quota
staff_count = self.staff.count()
if staff_count >= quota:
return True
return False
@property
def vat_rate(self):
return VatRate.objects.get(dealer=self, is_active=True).rate
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
indexes = [models.Index(fields=["name"])]
# permissions = [
# ('change_dealer_type', 'Can change dealer type'),
# ]
def __str__(self):
return self.name
@property
def invoices(self):
return Invoice.objects.filter(order__user=self.user)
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):
# staff_member = models.OneToOneField(
# StaffMember, on_delete=models.CASCADE, related_name="staff"
# )
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"),null=True,blank=True)
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
staff_type = models.CharField(
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/staff",
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(
max_length=255, unique=True, editable=False, null=True, blank=True
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.first_name}-{self.last_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
objects = StaffUserManager()
@property
def fullname(self):
return self.first_name + " " + self.last_name
def deactivate_account(self):
self.active = False
self.user.is_active = False
self.user.save()
self.save()
def activate_account(self):
self.active = True
self.user.is_active = True
self.user.save()
self.save()
def permenant_delete(self):
self.user.delete()
# self.staff_member.delete()
self.delete()
@property
def email(self):
return self.user.email
# @property
# def user(self):
# return self.staff_member.user
@property
def groups(self):
return CustomGroup.objects.select_related("group").filter(
pk__in=[x.customgroup.pk for x in self.user.groups.all()]
)
def clear_groups(self):
self.remove_superuser_permission()
return self.user.groups.clear()
def add_group(self, group, clean=False):
if clean:
self.clear_groups()
try:
self.user.groups.add(group)
if "accountant" in group.name.lower():
self.add_superuser_permission()
except Exception as e:
print(e)
def add_superuser_permission(self):
entity = self.dealer.entity
if entity.managers.count() == 0:
entity.managers.add(self.user)
def remove_superuser_permission(self):
entity = self.dealer.entity
if self.user in entity.managers.all():
entity.managers.remove(self.user)
class Meta:
verbose_name = _("Staff")
verbose_name_plural = _("Staff")
indexes = [
models.Index(fields=["staff_type"]),
]
permissions = []
constraints = [
models.UniqueConstraint(
fields=["dealer", "user"], name="unique_staff_email_per_dealer"
)
]
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Sources(models.TextChoices):
REFERRALS = "referrals", _("Referrals")
WHATSAPP = "whatsapp", _("WhatsApp")
SHOWROOM = "showroom", _("Showroom")
TIKTOK = "tiktok", _("TikTok")
INSTAGRAM = "instagram", _("Instagram")
X = "x", _("X")
FACEBOOK = "facebook", _("Facebook")
MOTORY = "motory", _("Motory")
INFLUENCERS = "influencers", _("Influencers")
YOUTUBE = "youtube", _("Youtube")
CAMPAIGN = "campaign", _("Campaign")
class Channel(models.TextChoices):
WALK_IN = "walk_in", _("Walk In")
TOLL_FREE = "toll_free", _("Toll Free")
WEBSITE = "website", _("Website")
EMAIL = "email", _("Email")
FORM = "form", _("Form")
class Status(models.TextChoices):
NEW = "new", _("New")
CONTACTED = "contacted", _("Contacted")
QUALIFIED = "qualified", _("Qualified")
UNQUALIFIED = "unqualified", _("Unqualified")
CONVERTED = "converted", _("Converted")
class Title(models.TextChoices):
MR = "mr", _("Mr")
MRS = "mrs", _("Mrs")
MS = "ms", _("Ms")
MISS = "miss", _("Miss")
DR = "dr", _("Dr")
PROF = "prof", _("Prof")
PRINCE = "prince", _("Prince")
PRINCESS = "princess", _("Princess")
COMPANY = "company", _("Company")
NA = "na", _("N/A")
class ActionChoices(models.TextChoices):
CALL = "call", _("Call")
SMS = "sms", _("SMS")
EMAIL = "email", _("Email")
MEETING = "meeting", _("Meeting")
WHATSAPP = "whatsapp", _("WhatsApp")
VISIT = "visit", _("Visit")
LEAD_NEGOTIATION = "negotiation", _("Negotiation")
LEAD_FOLLOW_UP = "follow_up", _("Follow Up")
LEAD_WON = "won", _("Won")
LEAD_LOST = "lost", _("Lost")
LEAD_CLOSED = "closed", _("Closed")
LEAD_CONVERTED = "converted", _("Converted")
LEAD_TRANSFER = "transfer", _("Transfer")
ADD_CAR = "add_car", _("Add Car")
SALE_CAR = "sale_car", _("Sale Car")
RESERVE_CAR = "reserve_car", _("Reserve Car")
TRANSFER_CAR = "transfer_car", _("Transfer Car")
REMOVE_CAR = "remove_car", _("Remove Car")
CREATE_QUOTATION = "create_quotation", _("Create Quotation")
CANCEL_QUOTATION = "cancel_quotation", _("Cancel Quotation")
CREATE_ORDER = "create_order", _("Create Order")
CANCEL_ORDER = "cancel_order", _("Cancel Order")
CREATE_INVOICE = "create_invoice", _("Create Invoice")
CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice")
class Stage(models.TextChoices):
QUALIFICATION = "qualification", _("Qualification")
TEST_DRIVE = "test_drive", _("Test Drive")
QUOTATION = "quotation", _("Quotation")
NEGOTIATION = "negotiation", _("Negotiation")
FINANCING = "financing", _("Financing")
CLOSED_WON = "closed_won", _("Closed Won")
CLOSED_LOST = "closed_lost", _("Closed Lost")
ON_HOLD = "on_hold", _("On Hold")
class Priority(models.TextChoices):
LOW = "low", _("Low")
MEDIUM = "medium", _("Medium")
HIGH = "high", _("High")
class Customer(models.Model):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="customers"
)
customer_model = models.ForeignKey(
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="customer_profile",
null=True,
blank=True,
)
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")
# )
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"),
)
dob = EncryptedDateField(verbose_name=_("Date of Birth"), null=True, blank=True)
email = EncryptedEmailField(verbose_name=_("Email"))
national_id = EncryptedCharField(
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
)
phone_number = EncryptedCharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
address = EncryptedCharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField(
upload_to="customers/",
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="image",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(
max_length=255, unique=True, editable=False, null=True, blank=True
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.last_name} {self.first_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["dealer", "email"], name="unique_customer_email_per_dealer"
)
]
verbose_name = _("Customer")
verbose_name_plural = _("Customers")
indexes = [
models.Index(fields=["title"]),
models.Index(fields=["first_name"]),
models.Index(fields=["last_name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
]
def __str__(self):
# middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name} {self.last_name}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def create_customer_model(self, for_lead=False):
customer_dict = to_dict(self)
customer = self.dealer.entity.get_customers().filter(email=self.email).first()
if not customer:
customer = self.dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": self.full_name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
},
)
try:
customer.additional_info.update({"customer_info": customer_dict})
except Exception:
pass
customer.active = False if for_lead else True
customer.save()
self.customer_model = customer
self.save()
return customer
def update_user_model(self):
user = self.user
user.first_name = self.first_name
user.last_name = self.last_name
user.email = self.email
user.save()
return user
def update_customer_model(self):
customer_dict = to_dict(self)
customer = self.customer_model
customer.customer_name = self.full_name
customer.address_1 = self.address
customer.phone = self.phone_number
customer.email = self.email
try:
customer.additional_info.update({"customer_info": customer_dict})
except Exception:
pass
customer.save()
return customer
def create_user_model(self, for_lead=False):
user, created = User.objects.get_or_create(
username=self.email,
defaults={
"email": self.email,
"first_name": self.first_name,
"last_name": self.last_name,
"password": make_random_password(),
"is_staff": False,
"is_superuser": False,
"is_active": False if for_lead else True,
},
)
self.user = user
self.save()
return user
def deactivate_account(self):
self.active = False
self.customer_model.active = False
# self.user.is_active = False
self.customer_model.save()
# self.user.save()
self.save()
def activate_account(self):
self.active = True
self.customer_model.active = True
# self.user.is_active = True
self.customer_model.save()
# self.user.save()
self.save()
def permenant_delete(self):
self.customer_model.delete()
# self.user.delete()
self.delete()
class Organization(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="organizations"
)
customer_model = models.ForeignKey(
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="organization_profile",
null=True,
blank=True,
)
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"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
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"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(
max_length=255, unique=True, editable=False, null=True, blank=True
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Organization")
verbose_name_plural = _("Organizations")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
]
def __str__(self):
return self.name
def create_customer_model(self, for_lead=False):
customer_dict = to_dict(self)
customer = self.dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": self.name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
},
)
try:
customer.additional_info.update({"customer_info": customer_dict})
except Exception:
pass
customer.active = False if for_lead else True
customer.save()
self.customer_model = customer
self.save()
return customer
def update_user_model(self):
user = self.user
user.first_name = self.name
user.email = self.email
user.save()
return user
def update_customer_model(self):
customer_dict = to_dict(self)
customer = self.customer_model
customer.customer_name = self.name
customer.address_1 = self.address
customer.phone = self.phone_number
customer.email = self.email
try:
customer.additional_info.update({"customer_info": customer_dict})
except Exception:
pass
customer.save()
return customer
def create_user_model(self, for_lead=False):
user = User.objects.create_user(
username=self.email,
email=self.email,
first_name=self.name,
password=make_random_password(),
is_staff=False,
is_superuser=False,
is_active=False if for_lead else True,
)
self.user = user
self.save()
return user
def deactivate_account(self):
self.active = False
# self.user.is_active = False
self.customer_model.active = False
# self.user.save()
self.customer_model.save()
self.save()
def activate_account(self):
self.active = True
self.customer_model.active = True
# self.user.is_active = True
self.customer_model.save()
# self.user.save()
self.save()
def permenant_delete(self):
# self.user.delete()
self.customer_model.delete()
self.delete()
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, unique=True, verbose_name=_("ID Number")
)
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
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 Lead(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads")
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
lead_type = models.CharField(
max_length=50,
choices=[("customer", _("Customer")), ("organization", _("Organization"))],
verbose_name=_("Lead Type"),
default="customer",
)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name="customer_leads",
null=True,
blank=True,
)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="organization_leads",
null=True,
blank=True,
)
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"),
)
source = models.CharField(
max_length=50, choices=Sources.choices, verbose_name=_("Source")
)
channel = models.CharField(
max_length=50, choices=Channel.choices, verbose_name=_("Channel")
)
staff = models.ForeignKey(
Staff,
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="assigned",
verbose_name=_("Assigned"),
)
status = models.CharField(
max_length=50,
choices=Status.choices,
verbose_name=_("Status"),
db_index=True,
default=Status.NEW,
)
next_action = models.CharField(
max_length=255, verbose_name=_("Next Action"), blank=True, null=True
)
next_action_date = models.DateTimeField(
verbose_name=_("Next Action Date"), blank=True, null=True
)
is_converted = models.BooleanField(default=False)
converted_at = models.DateTimeField(null=True, blank=True)
created = models.DateTimeField(
auto_now_add=True, verbose_name=_("Created"), db_index=True
)
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(unique=True, blank=True, null=True)
class Meta:
verbose_name = _("Lead")
verbose_name_plural = _("Leads")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["organization"]),
models.Index(fields=["staff"]),
models.Index(fields=["first_name"]),
models.Index(fields=["last_name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
models.Index(fields=["created"]),
]
def __str__(self):
return f"{self.first_name} {self.last_name}"
def get_user_model(self):
return User.objects.get(email=self.email) or None
@property
def activities(self):
return Activity.objects.filter(dealer=self.dealer, object_id=self.id)
def to_dict(self):
return {
"first_name": str(self.first_name),
"last_name": str(self.last_name),
"email": str(self.email),
"phone_number": str(self.phone_number),
"make": str(self.id_car_make.name),
"model": str(self.id_car_model.name),
"created_at": str(self.created.strftime("%Y-%m-%d")),
}
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def convert_to_customer(self):
self.status = Status.CONVERTED
self.is_converted = True
self.converted_at = datetime.now()
if self.customer:
self.customer.activate_account()
self.customer.save()
if self.organization:
self.organization.activate_account()
self.organization.save()
self.save()
return self.get_customer_model()
@property
def needs_follow_up(self):
latest = self.activities.order_by("-updated").first()
if not latest:
return True
return (timezone.now() - latest.updated).days > 3
@property
def stale_leads(self):
latest = self.activities.order_by("-updated").first()
if not latest:
return True
return (timezone.now() - latest.updated).days > 7
def get_customer_model(self):
if self.customer:
return self.customer.customer_model
if self.organization:
return self.organization.customer_model
def get_latest_schedule(self):
return self.schedules.order_by("-scheduled_at").first()
def get_latest_schedules(self):
return (
self.schedules.filter(scheduled_at__gt=now())
.exclude(status="Canceled")
.order_by("-scheduled_at")[:5]
)
def get_all_schedules(self):
return self.schedules.all().order_by("-scheduled_at")
def get_calls(self):
return self.get_all_schedules().filter(scheduled_type="call")
def get_meetings(self):
return self.get_all_schedules().filter(scheduled_type="meeting")
def get_emails(self):
return Email.objects.filter(content_type__model="lead", object_id=self.pk)
def get_notes(self):
return Notes.objects.filter(content_type__model="lead", object_id=self.pk)
def get_activities(self):
return Activity.objects.filter(
dealer=self.dealer, content_type__model="lead", object_id=self.pk
).order_by("-updated")
def get_opportunities(self):
return Opportunity.objects.filter(lead=self)
@property
def get_current_action(self):
return (
Activity.objects.filter(
dealer=self.dealer, content_type__model="lead", object_id=self.pk
)
.order_by("-updated")
.first()
)
def get_absolute_url(self):
return reverse("lead_detail", args=[self.dealer.slug, self.slug])
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.last_name} {self.first_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Schedule(models.Model):
PURPOSE_CHOICES = [
("product_demo", _("Product Demo")),
("follow_up_call", _("Follow-Up Call")),
("contract_discussion", _("Contract Discussion")),
("sales_meeting", _("Sales Meeting")),
("support_call", _("Support Call")),
("other", _("Other")),
]
ScheduledType = [
("call", _("Call")),
("meeting", _("Meeting")),
("email", _("Email")),
]
ScheduleStatusChoices = [
("scheduled", _("Scheduled")),
("completed", _("Completed")),
("canceled", _("Canceled")),
]
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
customer = models.ForeignKey(
CustomerModel,
on_delete=models.CASCADE,
related_name="schedules",
null=True,
blank=True,
verbose_name=_("Customer"),
)
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
purpose = models.CharField(
max_length=200,
choices=PURPOSE_CHOICES,
verbose_name=_("Purpose"),
help_text=_("What is the purpose of this schedule?"),
)
scheduled_at = models.DateTimeField(verbose_name=_("Scheduled Date"))
start_time = models.TimeField(
verbose_name=_("Start Time"), null=True, blank=True, help_text=_("HH:MM")
)
end_time = models.TimeField(
verbose_name=_("End Time"), null=True, blank=True, help_text=_("HH:MM")
)
scheduled_type = models.CharField(
max_length=200,
choices=ScheduledType,
default="Call",
verbose_name=_("Scheduled Type"),
help_text=_("What type of schedule is this?"),
)
completed = models.BooleanField(
default=False,
verbose_name=_("Completed"),
help_text=_("Has this schedule been completed?"),
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
status = models.CharField(
max_length=200,
choices=ScheduleStatusChoices,
default="Scheduled",
verbose_name=_("Status"),
help_text=_("What is the status of this schedule?"),
)
created_at = models.DateTimeField(
auto_now_add=True, verbose_name=_("Created Date"), help_text=_("When was this schedule created?")
)
updated_at = models.DateTimeField(
auto_now=True, verbose_name=_("Updated Date"), help_text=_("When was this schedule last updated?")
)
def __str__(self):
return f"Scheduled {self.purpose} on {self.scheduled_at}"
@property
def duration(self):
return (self.end_time - self.start_time).seconds
@property
def schedule_past_date(self):
if self.scheduled_at < now():
return True
return False
@property
def get_purpose(self):
return self.purpose.replace("_", " ").title()
class Meta:
ordering = ["-scheduled_at"]
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["content_type", "object_id"]),
models.Index(fields=["scheduled_at"]),
]
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"
)
changed_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Changed At"))
class Meta:
verbose_name = _("Lead Status History")
verbose_name_plural = _("Lead Status Histories")
def __str__(self):
return f"{self.lead}: {self.old_status}{self.new_status}"
def validate_probability(value):
if value < 0 or value > 100:
raise ValidationError(_("Probability must be between 0 and 100."))
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",
null=True,
blank=True,
)
organization = models.ForeignKey(
Organization,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("Organization"),
)
car = models.ForeignKey(
Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")
)
crn = models.CharField(max_length=20, verbose_name=_("CRN"), blank=True, null=True)
vrn = models.CharField(max_length=20, verbose_name=_("VRN"), blank=True, null=True)
salary = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("Salary"), blank=True, null=True
)
priority = models.CharField(
max_length=20,
choices=[("high", "High"), ("medium", "Medium"), ("low", "Low")],
verbose_name=_("Priority"),
default="medium",
)
stage = models.CharField(
max_length=20, choices=Stage.choices, verbose_name=_("Stage")
)
staff = models.ForeignKey(
Staff,
on_delete=models.SET_NULL,
null=True,
related_name="owner",
verbose_name=_("Owner"),
)
lead = models.OneToOneField(
"Lead",
related_name="opportunity",
on_delete=models.CASCADE,
verbose_name=_("Lead"),
null=True,
blank=True,
)
expected_revenue = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name=_("Expected Revenue"),
blank=True,
null=True,
)
expected_close_date = models.DateField(blank=True, null=True)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
estimate = models.OneToOneField(
EstimateModel,
related_name="opportunity",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
slug = models.SlugField(
null=True,
blank=True,
unique=True,
verbose_name=_("Slug"),
help_text=_("Unique slug for the opportunity."),
)
loss_reason = models.CharField(max_length=255, blank=True, null=True)
def get_notes(self):
return self._get_filter(Notes).order_by("-created")
def get_activities(self):
return self._get_filter(Activity)
def get_tasks(self):
return self._get_filter(Tasks)
def get_meetings(self):
return self.lead.get_meetings()
def get_calls(self):
return self.lead.get_calls()
def get_schedules(self):
# qs = Schedule.objects.filter(
# dealer=self.dealer,
# content_type__model__in=["lead"], object_id=self.object.id,
# scheduled_by=self.request.user
# )
return (
self.lead.get_all_schedules()
.filter(scheduled_at__gt=timezone.now())
.order_by("scheduled_at")
)
def get_emails(self):
return self._get_filter(Email)
def _get_filter(self, Model):
objects = Model.objects.filter(
content_type__model="opportunity", object_id=self.id
)
lead_objects = Model.objects.filter(
content_type__model="lead", object_id=self.lead.id
)
objects = objects.union(lead_objects).order_by("-created")
return objects
def save(self, *args, **kwargs):
opportinity_for = ""
if self.lead.lead_type == "customer":
self.customer = self.lead.customer
opportinity_for = self.customer.first_name + " " + self.customer.last_name
elif self.lead.lead_type == "organization":
self.organization = self.lead.organization
opportinity_for = self.organization.name
if not self.slug:
base_slug = slugify(f"opportinity {opportinity_for}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Opportunity")
verbose_name_plural = _("Opportunities")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["car"]),
models.Index(fields=["lead"]),
models.Index(fields=["organization"]),
models.Index(fields=["created"]),
]
def __str__(self):
if self.customer:
return (
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
)
return f"Opportunity for {self.organization.name}"
class Notes(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField()
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 = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
class Meta:
verbose_name = _("Note")
verbose_name_plural = _("Notes")
indexes = [
models.Index(fields=["dealer"], name="note_dealer_idx"),
models.Index(fields=["created_by"], name="note_created_by_idx"),
models.Index(fields=["content_type"], name="note_content_type_idx"),
models.Index(
fields=["content_type", "object_id"], name="note_content_object_idx"
),
models.Index(fields=["created"], name="note_created_date_idx"),
models.Index(fields=["updated"], name="note_updated_date_idx"),
models.Index(fields=["dealer", "created"], name="note_dealer_created_idx"),
models.Index(
fields=["content_type", "object_id", "created"],
name="note_content_obj_created_idx",
),
]
def __str__(self):
return f"Note by {self.created_by.first_name} on {self.content_object}"
class Tasks(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="tasks")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField()
content_object = GenericForeignKey("content_type", "object_id")
title = models.CharField(max_length=255, verbose_name=_("Title"))
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
due_date = models.DateField(verbose_name=_("Due Date"))
start_time = models.TimeField(verbose_name=_("Start Time"), null=True, blank=True)
end_time = models.TimeField(verbose_name=_("End Time"), null=True, blank=True)
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
assigned_to = models.ForeignKey(
User,
on_delete=models.DO_NOTHING,
related_name="tasks_assigned",
null=True,
blank=True,
)
created_by = models.ForeignKey(
User, on_delete=models.DO_NOTHING, related_name="tasks_created"
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
class Meta:
verbose_name = _("Task")
verbose_name_plural = _("Tasks")
indexes = [
models.Index(fields=["dealer"], name="task_dealer_idx"),
models.Index(fields=["created_by"], name="task_created_by_idx"),
models.Index(fields=["content_type"], name="task_content_type_idx"),
models.Index(
fields=["content_type", "object_id"], name="task_content_object_idx"
),
models.Index(fields=["created"], name="task_created_date_idx"),
models.Index(fields=["updated"], name="task_updated_date_idx"),
models.Index(fields=["dealer", "created"], name="task_dealer_created_idx"),
models.Index(
fields=["content_type", "object_id", "created"],
name="task_content_obj_created_idx",
),
]
def __str__(self):
return f"Task by {self.created_by.email} on {self.content_object}"
class Email(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField()
content_object = GenericForeignKey("content_type", "object_id")
from_email = models.TextField(verbose_name=_("From Email"), null=True, blank=True)
to_email = models.TextField(verbose_name=_("To Email"), null=True, blank=True)
subject = models.TextField(verbose_name=_("Subject"), null=True, blank=True)
message = models.TextField(verbose_name=_("Message"), null=True, blank=True)
status = models.CharField(
max_length=20,
choices=EmailStatus.choices,
verbose_name=_("Status"),
default=EmailStatus.OPEN,
)
created_by = models.ForeignKey(
User, on_delete=models.DO_NOTHING, related_name="emails_created"
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
class Meta:
verbose_name = _("Email")
verbose_name_plural = _("Emails")
indexes = [
models.Index(fields=["created_by"], name="email_created_by_idx"),
models.Index(fields=["content_type"], name="email_content_type_idx"),
models.Index(
fields=["content_type", "object_id"], name="email_content_object_idx"
),
models.Index(fields=["created"], name="email_created_date_idx"),
models.Index(fields=["updated"], name="email_updated_date_idx"),
models.Index(
fields=["content_type", "object_id", "created"],
name="email_content_obj_created_idx",
),
]
def __str__(self):
return f"Email by {self.created_by.first_name} on {self.content_object}"
class Activity(models.Model):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="activities"
)
content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
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")
)
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
created_by = models.ForeignKey(
User, on_delete=models.DO_NOTHING, related_name="activities_created_by"
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
class Meta:
verbose_name = _("Activity")
verbose_name_plural = _("Activities")
indexes = [
models.Index(fields=["created_by"], name="activity_created_by_idx"),
models.Index(fields=["content_type"], name="activity_content_type_idx"),
models.Index(
fields=["content_type", "object_id"], name="activity_content_object_idx"
),
models.Index(fields=["created"], name="activity_created_date_idx"),
models.Index(fields=["updated"], name="activity_updated_date_idx"),
models.Index(
fields=["content_type", "object_id", "created"],
name="a_content_obj_created_idx",
),
]
def __str__(self):
return f"{self.get_activity_type_display()} by {self.created_by.get_full_name} on {self.content_object}"
class Notification(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="notifications"
)
message = models.TextField(verbose_name=_("Message"))
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
class Meta:
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
ordering = ["-created"]
indexes = [
models.Index(fields=["user"], name="notification_user_idx"),
models.Index(fields=["is_read"], name="notification_is_read_idx"),
models.Index(fields=["created"], name="notification_created_date_idx"),
]
def __str__(self):
return self.message
@classmethod
def has_new_notifications(cls, user):
return cls.objects.filter(user=user, is_read=False).exists()
@classmethod
def get_notification_data(cls, user):
return cls.objects.filter(user=user)
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")
)
vendor_model = models.ForeignKey(
VendorModel,
on_delete=models.DO_NOTHING,
verbose_name=_("Vendor Model"),
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"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField(
upload_to="logos/vendors",
blank=True,
null=True,
verbose_name=_("Logo"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
slug = models.SlugField(
max_length=255, unique=True, verbose_name=_("Slug"), null=True, blank=True
)
def get_absolute_url(self):
return reverse(
"vendor_detail", kwargs={"dealer_slug": self.dealer.slug, "slug": self.slug}
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")
indexes = [
models.Index(fields=["slug"], name="vendor_slug_idx"),
models.Index(fields=["active"], name="vendor_active_idx"),
models.Index(fields=["crn"], name="vendor_crn_idx"),
models.Index(fields=["vrn"], name="vendor_vrn_idx"),
]
def __str__(self):
return self.name
def create_vendor_model(self):
entity = self.dealer.entity
additionals = to_dict(self)
if not self.vendor_model:
vendor = entity.create_vendor(
vendor_model_kwargs={
"vendor_name": self.name,
"vendor_number": self.crn,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
"tax_id_number": self.vrn,
"active": True,
"hidden": False,
"additional_info": additionals,
}
)
self.vendor_model = vendor
self.save()
def update_vendor_model(self):
additionals = to_dict(self)
self.vendor_model.vendor_name = self.name
self.vendor_model.vendor_number = self.crn
self.vendor_model.address_1 = self.address
self.vendor_model.phone = self.phone_number
self.vendor_model.email = self.email
self.vendor_model.tax_id_number = self.vrn
self.vendor_model.additional_info = additionals
self.vendor_model.save()
def create_vendor_account(self, role):
entity = self.dealer.entity
coa = entity.get_default_coa()
last_account = (
entity.get_all_accounts().filter(role=role).order_by("-created").first()
)
if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}"
elif len(last_account.code) > 4:
code = f"{int(last_account.code) + 1}"
if (
not entity.get_all_accounts()
.filter(
name=self.name,
role=role,
coa_model=coa,
balance_type="credit",
active=True,
)
.exists()
):
entity.create_account(
name=self.name,
code=code,
role=role,
coa_model=coa,
balance_type="credit",
active=True,
)
def activate_account(self):
self.active = True
self.vendor_model.active = True
self.save()
def permenant_delete(self):
self.vendor_model.delete()
self.delete()
class Payment(models.Model):
METHOD_CHOICES = [
("cash", _("cash")),
("credit", _("credit")),
("transfer", _("transfer")),
("debit", _("debit")),
("sadad", _("SADAD")),
]
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"))
invoice = models.ForeignKey(
InvoiceModel,
on_delete=models.CASCADE,
related_name="payments",
verbose_name=_("invoice"),
null=True,
blank=True,
)
class Meta:
verbose_name = _("payment")
verbose_name_plural = _("payments")
def __str__(self):
return f"Payment of {self.amount} on {self.payment_date}"
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}"
class SaleOrder(models.Model):
STATUS_CHOICES = [
("PENDING_APPROVAL", "Pending Approval"),
("APPROVED", "Approved"),
("IN_FINANCING", "In Financing"),
("PARTIALLY_PAID", "Partially Paid"),
("FULLY_PAID", "Fully Paid"),
("PENDING_DELIVERY", "Pending Delivery"),
("DELIVERED", "Delivered"),
("CANCELLED", "Cancelled"),
]
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sale_orders"
)
estimate = models.ForeignKey(
EstimateModel,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Estimate"),
null=True,
blank=True,
)
invoice = models.ForeignKey(
InvoiceModel,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Invoice"),
null=True,
blank=True,
)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Customer"),
null=True,
blank=True,
)
opportunity = models.ForeignKey(
Opportunity,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Opportunity"),
null=True,
blank=True,
)
comments = models.TextField(blank=True, null=True)
formatted_order_id = models.CharField(max_length=255, unique=True, editable=False)
# Status and Dates
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default="PENDING_APPROVAL",
help_text="Current status of the sales order.",
)
order_date = models.DateTimeField(
default=timezone.now, help_text="The date and time the sales order was created."
)
expected_delivery_date = models.DateField(
blank=True, null=True, help_text="The planned date for vehicle delivery."
)
actual_delivery_date = models.DateTimeField(
blank=True,
null=True,
help_text="The actual date and time the vehicle was delivered.",
)
cancelled_date = models.DateTimeField(
blank=True,
null=True,
help_text="The date and time the order was cancelled, if applicable.",
)
cancellation_reason = models.TextField(
blank=True, null=True, help_text="Reason for cancellation, if applicable."
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="created_sales_orders",
help_text="The user who created this sales order.",
)
last_modified_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="modified_sales_orders",
help_text="The user who last modified this sales order.",
)
class Meta:
verbose_name = _("Sales Order")
verbose_name_plural = _("Sales Orders")
ordering = ["-order_date"] # Order by most recent first
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["estimate"]),
models.Index(fields=["invoice"]),
models.Index(fields=["opportunity"]),
models.Index(fields=["customer"]),
models.Index(fields=["status"]),
models.Index(fields=["order_date"]),
models.Index(fields=["expected_delivery_date"]),
models.Index(fields=["actual_delivery_date"]),
models.Index(fields=["cancelled_date"]),
]
def save(self, *args, **kwargs):
if not self.formatted_order_id:
last_order = SaleOrder.objects.order_by("-id").first()
if last_order:
next_id = last_order.id + 1
else:
next_id = 1
year = get_localdate().year
self.formatted_order_id = f"O-{year}-{next_id:09d}"
super().save(*args, **kwargs)
def __str__(self):
return f"Sales Order #{self.formatted_order_id} for {self.customer.first_name} "
@property
def full_name(self):
return f"{self.customer.first_name} {self.customer.last_name}"
@property
def price(self):
return self.car.marked_price
@property
def items(self):
# Check if an invoice is associated with this SaleOrder
if self.invoice:
# Check if get_itemtxs_data returns data before proceeding
# You might want to handle what get_itemtxs_data returns if it can be empty
item_data = self.estimate.get_itemtxs_data()[0]
if item_data:
return item_data
return [] # Return an empty list if no invoice or no item data
@property
def cars(self):
# Check if self.items is not empty before trying to iterate
if (
self.items.exists() if hasattr(self.items, "exists") else self.items
): # Handle both QuerySet and list
# Ensure x is an *instance* of ItemTransactionModel
# item_model should be a ForeignKey to your CarModel within ItemTransactionModel
return [
x.item_model.car
for x in self.items
if hasattr(x, "item_model") and hasattr(x.item_model, "car")
]
return [] # Return an empty list if no items or no associated cars
class CustomGroup(models.Model):
name = models.CharField(max_length=100)
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="groups")
group = models.OneToOneField(
"auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE
)
class Meta:
verbose_name = _("Custom Group")
verbose_name_plural = _("Custom Groups")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["dealer"]),
models.Index(fields=["group"]),
]
@property
def entity(self):
return self.dealer.entity
@property
def users(self):
return self.group.user_set.exclude(email=self.dealer.user.email).all()
@property
def permissions(self):
return self.group.permissions.all()
def clear_permissions(self):
self.group.permissions.clear()
def add_permission(self, permission):
try:
self.group.permissions.add(permission)
except Permission.DoesNotExist:
pass
def __str__(self):
return self.name
def set_default_manager_permissions(self):
self.clear_permissions()
try:
for perm in Permission.objects.filter(content_type__app_label="inventory"):
self.add_permission(perm)
except Exception:
pass
def set_default_permissions(self):
self.clear_permissions()
######################################
######################################
# MANAGER
######################################
######################################
if self.name == "Manager":
self.set_permissions(
app="inventory",
allowed_models=[
"car",
"carfinance",
"carlocation",
"customcard",
"cartransfer",
"carcolors",
"carequipment",
"interiorcolors",
"exteriorcolors",
"carreservation",
"lead",
"customgroup",
"saleorder",
"payment",
# "staff",
"schedule",
"activity",
"opportunity",
"vendor",
"customer",
"notes",
"tasks",
"activity",
"additionalservices"
],
)
self.set_permissions(
app="django_ledger",
allowed_models=[
"estimatemodel",
"invoicemodel",
"accountmodel",
"chartofaccountmodel",
"customermodel",
"billmodel",
"bankaccountmodel",
"itemmodel",
"vendormodel",
"journalentrymodel",
"purchaseordermodel",
"ledgermodel",
"transactionmodel",
],
other_perms=[
"can_approve_estimatemodel",
"can_approve_billmodel",
"can_view_inventory",
"can_view_sales",
"can_view_crm",
"can_view_financials",
"can_view_reports",
],
)
######################################
######################################
# Inventory
######################################
######################################
elif self.name == "Inventory":
self.set_permissions(
app="inventory",
allowed_models=[
"car",
"carequipment",
"interiorcolors",
"exteriorcolors",
"carcolors",
"carlocation",
"customcard",
"carreservation",
"notes",
"tasks",
"activity",
"poitemsuploaded",
],
)
self.set_permissions(
app="django_ledger",
allowed_models=[],
other_perms=[
"view_purchaseordermodel",
],
)
######################################
######################################
# Sales
######################################
######################################
elif self.name == "Sales":
self.set_permissions(
app="django_ledger",
allowed_models=["invoicemodel", "customermodel"],
)
self.set_permissions(
app="inventory",
allowed_models=[
"saleorder",
# "payment",
# "staff",
"schedule",
"activity",
"lead",
"opportunity",
"customer",
"organization",
"notes",
"tasks",
"leadactivity",
],
other_perms=[
"view_car",
"view_carlocation",
"view_customcard",
"view_carcolors",
"view_cartransfer",
"can_view_inventory",
"can_view_sales",
"can_view_crm",
"view_estimatemodel",
"add_estimatemodel",
"change_estimatemodel",
"delete_estimatemodel",
],
)
######################################
######################################
# Accountant
######################################
######################################
elif self.name == "Accountant":
self.set_permissions(
app="inventory",
allowed_models=[
"carfinance",
"notes",
"tasks",
"activity",
"payment",
"vendor",
"additionalservices",
'customer'
],
other_perms=[
"view_car",
"view_carlocation",
"view_customcard",
"view_carcolors",
"view_cartransfer",
"view_saleorder",
"view_leads",
"view_opportunity",
"view_customer",
],
)
self.set_permissions(
app="django_ledger",
allowed_models=[
"bankaccountmodel",
"accountmodel",
"chartofaccountmodel",
"itemmodel",
"invoicemodel",
"vendormodel",
"journalentrymodel",
"purchaseordermodel",
"estimatemodel",
"customermodel",
"ledgermodel",
"transactionmodel",
],
other_perms=[
"view_billmodel",
"add_billmodel",
"change_billmodel",
"delete_billmodel",
"view_customermodel",
"view_estimatemodel",
"can_view_inventory",
"can_view_sales",
"can_view_crm",
"can_view_financials",
"can_view_reports",
],
)
def set_permissions(self, app="inventory", allowed_models=[], other_perms=[]):
try:
for perm in Permission.objects.filter(
content_type__app_label=app, content_type__model__in=allowed_models
):
self.add_permission(perm)
for perm in other_perms:
p = Permission.objects.get(codename=perm)
self.add_permission(p)
except Exception as e:
print(e)
class DealerSettings(models.Model):
dealer = models.OneToOneField(
Dealer, on_delete=models.CASCADE, related_name="settings", null=True, blank=True
)
invoice_cash_account = models.ForeignKey(
AccountModel,
related_name="invoice_cash",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
invoice_prepaid_account = models.ForeignKey(
AccountModel,
related_name="invoice_prepaid",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
invoice_unearned_account = models.ForeignKey(
AccountModel,
related_name="invoice_unearned",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
bill_cash_account = models.ForeignKey(
AccountModel,
related_name="bill_cash",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
bill_prepaid_account = models.ForeignKey(
AccountModel,
related_name="bill_prepaid",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
bill_unearned_account = models.ForeignKey(
AccountModel,
related_name="bill_unearned",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
additional_info = models.JSONField(default=dict, null=True, blank=True)
def __str__(self):
return f"Settings for {self.dealer}"
# class customPlan(AbstractPlan):
# default = models.BooleanField(
# help_text=_('Both "Unknown" and "No" means that the plan is not default'),
# default=None,
# db_index=True,
# unique=False,
# null=True,
# )
class PaymentHistory(models.Model):
# Payment status choices
INITIATED = "initiated"
PENDING = "pending"
PAID = "paid"
COMPLETED = "completed"
FAILED = "failed"
REFUNDED = "refunded"
CANCELLED = "cancelled"
PAYMENT_STATUS_CHOICES = [
(INITIATED, "initiated"),
(PENDING, "Pending"),
(COMPLETED, "Completed"),
(PAID, "Paid"),
(FAILED, "Failed"),
(REFUNDED, "Refunded"),
(CANCELLED, "Cancelled"),
]
# Payment method choices
CREDIT_CARD = "credit_card"
DEBIT_CARD = "debit_card"
PAYPAL = "paypal"
BANK_TRANSFER = "bank_transfer"
CRYPTO = "crypto"
OTHER = "other"
PAYMENT_METHOD_CHOICES = [
(CREDIT_CARD, "Credit Card"),
(DEBIT_CARD, "Debit Card"),
(PAYPAL, "PayPal"),
(BANK_TRANSFER, "Bank Transfer"),
(CRYPTO, "Cryptocurrency"),
(OTHER, "Other"),
]
# Basic payment information
user = models.ForeignKey(
"auth.User", # or your custom user model
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="payments",
)
user_data = models.JSONField(null=True, blank=True)
amount = models.DecimalField(
max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)]
)
currency = models.CharField(max_length=3, default="SAR")
payment_date = models.DateTimeField(default=timezone.now)
status = models.CharField(
max_length=10, choices=PAYMENT_STATUS_CHOICES, default=PENDING
)
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES)
# Transaction references
transaction_id = models.CharField(
max_length=100, unique=True, blank=True, null=True
)
invoice_number = models.CharField(max_length=50, blank=True, null=True)
order_reference = models.CharField(max_length=100, blank=True, null=True)
# Payment processor details
gateway_response = models.JSONField(
blank=True, null=True
) # Raw response from payment gateway
gateway_name = models.CharField(max_length=50, blank=True, null=True)
# Additional metadata
description = models.TextField(blank=True, null=True)
is_recurring = models.BooleanField(default=False)
billing_email = models.EmailField(blank=True, null=True)
billing_address = models.TextField(blank=True, null=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("Payment History")
verbose_name_plural = _("Payment Histories")
ordering = ["-payment_date"]
indexes = [
models.Index(fields=["transaction_id"]),
models.Index(fields=["user"]),
models.Index(fields=["status"]),
models.Index(fields=["payment_date"]),
]
def __str__(self):
return f"Payment #{self.id} - {self.amount} {self.currency} ({self.status})"
def is_successful(self):
return self.status == self.COMPLETED
######################################################################################################
######################################################################################################
######################################################################################################
class PoItemsUploaded(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, null=True, blank=True)
po = models.ForeignKey(
PurchaseOrderModel,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="items",
)
item = models.ForeignKey(
ItemTransactionModel,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="po_items",
)
status = models.CharField(max_length=100, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("PO Items")
verbose_name_plural = _("PO Items")
indexes = [
models.Index(fields=["po"]),
models.Index(fields=["item"]),
]
def get_name(self):
return self.item.item.name.split("||")
class ExtraInfo(models.Model):
"""
Stores additional information for any model with:
- Multiple generic relationships
- JSON data storage
- Tracking fields
"""
dealer = models.ForeignKey(
Dealer,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="extra_info",
)
content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="extra_info_primary"
)
object_id = models.CharField(max_length=255, null=True, blank=True)
content_object = GenericForeignKey("content_type", "object_id")
# Secondary GenericForeignKey (optional additional link)
related_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="extra_info_secondary",
)
related_object_id = models.CharField(max_length=255, null=True, blank=True)
related_object = GenericForeignKey("related_content_type", "related_object_id")
# JSON Data Storage
data = models.JSONField(encoder=DjangoJSONEncoder, default=dict, blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="created_extra_info"
)
class Meta:
indexes = [
models.Index(fields=["content_type", "object_id"]),
models.Index(fields=["related_content_type", "related_object_id"]),
]
verbose_name_plural = _("Extra Info")
verbose_name = _("Extra Info")
def __str__(self):
return f"ExtraInfo for {self.content_object} ({self.content_type})"
@classmethod
def get_sale_orders(cls, staff=None, is_dealer=False, dealer=None):
if not staff and not is_dealer:
return []
content_type = ContentType.objects.get_for_model(EstimateModel)
related_content_type = ContentType.objects.get_for_model(Staff)
if is_dealer:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id__isnull=False,
).union(
cls.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(User),
)
)
else:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id=staff.pk,
)
# qs = qs.select_related("customer","estimate","invoice")
data = SaleOrder.objects.filter(
pk__in=[
x.content_object.sale_orders.select_related(
"customer", "estimate", "invoice"
)
.first()
.pk
for x in qs
if x.content_object.sale_orders.first()
]
)
return data
# return [
# x.content_object.sale_orders.select_related(
# "customer", "estimate", "invoice"
# ).first()
# for x in qs
# if x.content_object.sale_orders.first()
# ]
@classmethod
def get_invoices(cls, staff=None, is_dealer=False, dealer=None):
if not staff and not is_dealer:
return []
content_type = ContentType.objects.get_for_model(EstimateModel)
related_content_type = ContentType.objects.get_for_model(Staff)
if is_dealer:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id__isnull=False,
).union(
cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=ContentType.objects.get_for_model(User),
)
)
else:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id=staff.pk,
)
return [
x.content_object.invoicemodel_set.first()
for x in qs
if x.content_object.invoicemodel_set.first()
]
class Recall(models.Model):
title = models.CharField(max_length=200, verbose_name=_("Recall Title"))
description = models.TextField(verbose_name=_("Description"))
make = models.ForeignKey(
CarMake, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True
)
model = models.ForeignKey(
CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True
)
serie = models.ForeignKey(
CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True
)
trim = models.ForeignKey(
CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True
)
year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True)
year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("Created By"),
)
class Meta:
verbose_name = _("Recall")
verbose_name_plural = _("Recalls")
def __str__(self):
return self.title
class RecallNotification(models.Model):
recall = models.ForeignKey(
Recall, on_delete=models.CASCADE, related_name="notifications"
)
dealer = models.ForeignKey(
"Dealer", on_delete=models.CASCADE, related_name="recall_notifications"
)
sent_at = models.DateTimeField(auto_now_add=True)
cars_affected = models.ManyToManyField(Car, related_name="recall_notifications")
class Meta:
verbose_name = _("Recall Notification")
verbose_name_plural = _("Recall Notifications")
def __str__(self):
return f"Notification for {self.dealer} about {self.recall}"
class Ticket(models.Model):
STATUS_CHOICES = [
("open", _("Open")),
("in_progress", _("In Progress")),
("resolved", _("Resolved")),
("closed", _("Closed")),
]
PRIORITY_CHOICES = [
("low", _("Low")),
("medium", _("Medium")),
("high", _("High")),
("critical", _("Critical")),
]
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="tickets", verbose_name=_("Dealer")
)
subject = models.CharField(
max_length=200, verbose_name=_("Subject"), help_text=_("Short description")
)
description = models.TextField(verbose_name=_("Description"))
resolution_notes = models.TextField(
blank=True, null=True, verbose_name=_("Resolution Notes")
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default="open",
verbose_name=_("Status"),
)
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default="medium",
verbose_name=_("Priority"),
)
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 time_to_resolution(self):
"""
Calculate the time taken to resolve the ticket.
Returns None if ticket isn't resolved/closed.
Returns timedelta if resolved/closed.
"""
if self.status in ["resolved", "closed"] and self.created_at:
return self.updated_at - self.created_at
return None
@property
def time_to_resolution_display(self):
"""
Returns a human-readable version of time_to_resolution
"""
resolution_time = self.time_to_resolution
if not resolution_time:
return "Not resolved yet"
days = resolution_time.days
hours, remainder = divmod(resolution_time.seconds, 3600)
minutes, _ = divmod(remainder, 60)
parts = []
if days > 0:
parts.append(f"{days} day{'s' if days != 1 else ''}")
if hours > 0:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0 or not parts:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return ", ".join(parts)
def __str__(self):
return f"#{self.id} - {self.subject} ({self.status})"
class CarImage(models.Model):
car = models.OneToOneField(
"Car", on_delete=models.CASCADE, related_name="generated_image"
)
image_hash = models.CharField(max_length=64, unique=True)
image = models.ImageField(upload_to="car_images/", null=True, blank=True)
is_generating = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def generate_hash(self):
"""Simple hash generation"""
car = self.car
hash_string = f"{car.id_car_make.name if car.id_car_make else ''}-{car.id_car_model.name if car.id_car_model else ''}-{car.year}-{getattr(car, 'color', 'default')}"
return hashlib.sha256(hash_string.encode()).hexdigest()
def schedule_generation(self):
"""Schedule image generation"""
from django_q.tasks import async_task
from inventory.tasks import generate_car_image_task
self.is_generating = True
self.save()
async_task(
generate_car_image_task,
self.id,
task_name=f"generate_car_image_{self.car.vin}",
)
class UserRegistration(models.Model):
name = models.CharField(_("Name"), max_length=255)
arabic_name = models.CharField(_("Arabic Name"), max_length=255)
email = models.EmailField(_("email address"), unique=True)
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
crn = models.CharField(_("Commercial Registration Number"), max_length=10, unique=True)
vrn = models.CharField(_("Vehicle Registration Number"), max_length=15, unique=True)
address = models.TextField(_("Address"))
password = models.CharField(_("Password"), max_length=255,null=True,blank=True)
is_created = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
REQUIRED_FIELDS = ["username", "arabic_name", "crn", "vrn", "address", "phone_number"]
def __str__(self):
return self.email
def create_account(self):
from .tasks import create_user_dealer
if self.is_created or User.objects.filter(email=self.email).exists():
logger.info(f"Account already created or exists: {self.email}")
return False
password = make_random_password()
try:
logger.info(f"Creating user account {self.email}")
dealer = create_user_dealer(
email=self.email,
password=password,
name=self.name,
arabic_name=self.arabic_name,
phone=self.phone_number,
crn=self.crn,
vrn=self.vrn,
address=self.address
)
if dealer:
self.is_created = True
self.password = password
self.save()
logger.info(f"User account created successfully: {self.email}")
return True
else:
logger.error(f"Failed to create dealer account: {self.email}")
return False
except Exception as e:
logger.error(f"Error creating account for {self.email}: {e}")
return False