3964 lines
130 KiB
Python
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 |