1854 lines
64 KiB
Python
1854 lines
64 KiB
Python
from django.contrib.auth.models import Permission
|
|
from decimal import Decimal
|
|
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,
|
|
)
|
|
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, to_dict
|
|
from .mixins import LocalizedNameMixin
|
|
from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel,AccountModel,EntityManagementModel
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from appointment.models import StaffMember
|
|
from plans.quota import get_user_quota
|
|
from plans.models import UserPlan,Quota,PlanQuota
|
|
# from plans.models import AbstractPlan
|
|
# from simple_history.models import HistoricalRecords
|
|
|
|
|
|
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):
|
|
rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.15"))
|
|
is_active = models.BooleanField(default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"Rate: {self.rate}%"
|
|
|
|
|
|
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)
|
|
arabic_name = models.CharField(max_length=255, blank=True, null=True)
|
|
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
|
|
is_sa_import = models.BooleanField(default=False)
|
|
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Make")
|
|
|
|
|
|
class CarModel(models.Model, LocalizedNameMixin):
|
|
id_car_model = models.AutoField(primary_key=True)
|
|
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
|
|
name = models.CharField(max_length=255, blank=True, null=True)
|
|
arabic_name = models.CharField(max_length=255, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Model")
|
|
|
|
|
|
class 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)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Series")
|
|
|
|
|
|
class CarTrim(models.Model, LocalizedNameMixin):
|
|
id_car_trim = models.AutoField(primary_key=True)
|
|
id_car_serie = models.ForeignKey(
|
|
CarSerie, models.DO_NOTHING, db_column="id_car_serie"
|
|
)
|
|
name = models.CharField(max_length=255, 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)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Trim")
|
|
|
|
|
|
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)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Equipment")
|
|
|
|
|
|
class CarSpecification(models.Model, LocalizedNameMixin):
|
|
id_car_specification = models.AutoField(primary_key=True)
|
|
name = models.CharField(max_length=255)
|
|
arabic_name = models.CharField(max_length=255)
|
|
id_parent = models.ForeignKey(
|
|
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Specification")
|
|
|
|
|
|
class CarSpecificationValue(models.Model):
|
|
id_car_specification_value = models.AutoField(primary_key=True)
|
|
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim")
|
|
id_car_specification = models.ForeignKey(
|
|
CarSpecification, models.DO_NOTHING, db_column="id_car_specification"
|
|
)
|
|
value = models.CharField(max_length=500)
|
|
unit = models.CharField(max_length=255, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.id_car_specification.name}: {self.value} {self.unit}"
|
|
|
|
class Meta:
|
|
verbose_name = _("Specification Value")
|
|
|
|
|
|
class 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
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = _("Option")
|
|
|
|
|
|
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")
|
|
|
|
|
|
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,
|
|
}
|
|
@property
|
|
def price_(self):
|
|
vat = VatRate.objects.filter(is_active=True).first()
|
|
return Decimal(self.price + (self.price * vat.rate)) if self.taxable else self.price
|
|
class Meta:
|
|
verbose_name = _("Additional Services")
|
|
verbose_name_plural = _("Additional Services")
|
|
|
|
def __str__(self):
|
|
return self.name + " - " + str(self.price)
|
|
|
|
|
|
class Car(models.Model):
|
|
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
|
|
dealer = models.ForeignKey(
|
|
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
|
|
)
|
|
|
|
vendor = models.ForeignKey(
|
|
"Vendor",
|
|
models.DO_NOTHING,
|
|
related_name="cars",
|
|
verbose_name=_("Vendor"),
|
|
)
|
|
id_car_make = models.ForeignKey(
|
|
CarMake,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_make",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Make"),
|
|
)
|
|
id_car_model = models.ForeignKey(
|
|
CarModel,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_model",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Model"),
|
|
)
|
|
year = models.IntegerField(verbose_name=_("Year"))
|
|
id_car_serie = models.ForeignKey(
|
|
CarSerie,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_serie",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Series"),
|
|
)
|
|
id_car_trim = models.ForeignKey(
|
|
CarTrim,
|
|
models.DO_NOTHING,
|
|
db_column="id_car_trim",
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Trim"),
|
|
)
|
|
status = models.CharField(
|
|
max_length=10,
|
|
choices=CarStatusChoices.choices,
|
|
default=CarStatusChoices.AVAILABLE,
|
|
verbose_name=_("Status"),
|
|
)
|
|
stock_type = models.CharField(
|
|
max_length=10,
|
|
choices=CarStockTypeChoices.choices,
|
|
default=CarStockTypeChoices.NEW,
|
|
verbose_name=_("Stock Type"),
|
|
)
|
|
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
|
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
|
|
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
|
|
hash = models.CharField(max_length=64, blank=True, null=True, verbose_name=_("Hash"))
|
|
# history = HistoricalRecords()
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.hash = self.get_hash
|
|
super(Car, self).save(*args, **kwargs)
|
|
|
|
class Meta:
|
|
verbose_name = _("Car")
|
|
verbose_name_plural = _("Cars")
|
|
|
|
def __str__(self):
|
|
make = self.id_car_make.name if self.id_car_make else "Unknown Make"
|
|
model = self.id_car_model.name if self.id_car_model else "Unknown Model"
|
|
trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
|
|
return f"{self.year} - {make} - {model} - {trim}"
|
|
|
|
def 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 ready(self):
|
|
try:
|
|
return all([self.colors ,self.finances,])
|
|
except Exception as e:
|
|
return False
|
|
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:
|
|
pass
|
|
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,request):
|
|
dealer = get_user_type(request)
|
|
self.cancel_reservation()
|
|
self.status = CarStatusChoices.SOLD
|
|
self.save()
|
|
Activity.objects.create(dealer=dealer,content_object=self, notes=_("Car Sold"),created_by=request.user,activity_type=ActionChoices.SALE_CAR)
|
|
|
|
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": self.id,
|
|
}
|
|
|
|
def get_specifications(self):
|
|
specs = CarSpecificationValue.objects.filter(id_car_trim=self.id_car_trim)
|
|
return specs
|
|
|
|
class CarTransfer(models.Model):
|
|
car = models.ForeignKey(
|
|
"Car",
|
|
on_delete=models.CASCADE,
|
|
related_name="transfer_logs",
|
|
verbose_name=_("Car"),
|
|
)
|
|
from_dealer = models.ForeignKey(
|
|
"Dealer",
|
|
on_delete=models.CASCADE,
|
|
related_name="transfers_out",
|
|
verbose_name=_("From Dealer"),
|
|
)
|
|
to_dealer = models.ForeignKey(
|
|
"Dealer",
|
|
on_delete=models.CASCADE,
|
|
related_name="transfers_in",
|
|
verbose_name=_("To Dealer"),
|
|
)
|
|
transfer_date = models.DateTimeField(
|
|
auto_now_add=True, verbose_name=_("Transfer Date")
|
|
)
|
|
quantity = models.IntegerField(verbose_name=_("Quantity"),default=1)
|
|
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
|
status = models.CharField(
|
|
CarTransferStatusChoices.choices,
|
|
max_length=10,
|
|
default=CarTransferStatusChoices.draft,
|
|
)
|
|
is_approved = models.BooleanField(default=False)
|
|
active = models.BooleanField(default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
|
|
|
@property
|
|
def total_price(self):
|
|
return self.quantity * self.car.finances.total_vat
|
|
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")
|
|
)
|
|
discount_amount = models.DecimalField(
|
|
max_digits=14,
|
|
decimal_places=2,
|
|
verbose_name=_("Discount Amount"),
|
|
default=Decimal("0.00"),
|
|
)
|
|
|
|
@property
|
|
def total(self):
|
|
return self.selling_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.selling_price - self.discount_amount
|
|
return self.selling_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(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.selling_price - self.cost_price
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"cost_price": str(self.cost_price),
|
|
"selling_price": str(self.selling_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}, Selling Price: {self.selling_price}"
|
|
|
|
# def save(self, *args, **kwargs):
|
|
# self.full_clean()
|
|
# try:
|
|
# price_after_discount = self.selling_price - self.discount_amount
|
|
# self.profit_margin = price_after_discount - self.cost_price
|
|
# self.vat_amount = settings.VAT_RATE
|
|
# except InvalidOperation as e:
|
|
# raise ValidationError(_("Invalid decimal operation: %s") % str(e))
|
|
# super().save(*args, **kwargs)
|
|
|
|
class Meta:
|
|
verbose_name = _("Car Financial Details")
|
|
verbose_name_plural = _("Car Financial Details")
|
|
|
|
|
|
class ExteriorColors(models.Model, LocalizedNameMixin):
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Exterior Colors")
|
|
verbose_name_plural = _("Exterior Colors")
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.rgb})"
|
|
|
|
|
|
class InteriorColors(models.Model, LocalizedNameMixin):
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
rgb = models.CharField(max_length=24, blank=True, null=True, verbose_name=_("RGB"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Interior Colors")
|
|
verbose_name_plural = _("Interior Colors")
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.rgb})"
|
|
|
|
|
|
class CarColors(models.Model):
|
|
car = models.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")
|
|
|
|
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 = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
logo = models.ImageField(
|
|
upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")
|
|
)
|
|
entity = models.ForeignKey(
|
|
EntityModel, on_delete=models.SET_NULL, null=True, blank=True
|
|
)
|
|
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
|
|
|
objects = DealerUserManager()
|
|
|
|
|
|
@property
|
|
def active_plan(self):
|
|
try:
|
|
plan = UserPlan.objects.get(user=self.user,active=True).plan
|
|
print(plan)
|
|
return plan
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
return None
|
|
|
|
@property
|
|
def user_quota(self):
|
|
try:
|
|
quota_dict = get_user_quota(self.user)
|
|
allowed_users = quota_dict.get("Users", None)
|
|
|
|
print(allowed_users)
|
|
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)
|
|
|
|
print(allowed_cars)
|
|
return allowed_cars
|
|
except Exception as e:
|
|
print(e)
|
|
return None
|
|
|
|
def get_vendors(self):
|
|
return VendorModel.objects.filter(entity_model=self.entity)
|
|
@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
|
|
|
|
class Meta:
|
|
verbose_name = _("Dealer")
|
|
verbose_name_plural = _("Dealers")
|
|
# permissions = [
|
|
# ('change_dealer_type', 'Can change dealer type'),
|
|
# ]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class StaffTypes(models.TextChoices):
|
|
# MANAGER = "manager", _("Manager")
|
|
INVENTORY = "inventory", _("Inventory")
|
|
ACCOUNTANT = "accountant", _("Accountant")
|
|
SALES = "sales", _("Sales")
|
|
# COORDINATOR = "coordinator", _("Coordinator")
|
|
# RECEPTIONIST = "receptionist", _("Receptionist")
|
|
# AGENT = "agent", _("Agent")
|
|
|
|
|
|
class Staff(models.Model, LocalizedNameMixin):
|
|
staff_member = models.OneToOneField(StaffMember, on_delete=models.CASCADE, related_name="staff")
|
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
staff_type = models.CharField(choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type"))
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
|
|
objects = StaffUserManager()
|
|
|
|
@property
|
|
def email(self):
|
|
return self.staff_member.user.email
|
|
@property
|
|
def user(self):
|
|
return self.staff_member.user
|
|
|
|
@property
|
|
def groups(self):
|
|
return [x.customgroup for x in self.user.groups.all()]
|
|
|
|
|
|
def clear_groups(self):
|
|
EntityManagementModel.objects.filter(user=self.user,entity=self.dealer.entity).delete()
|
|
return self.user.groups.clear()
|
|
|
|
def add_group(self,group):
|
|
try:
|
|
self.user.groups.add(group)
|
|
self.add_as_manager()
|
|
except Exception as e:
|
|
pass
|
|
def add_as_manager(self):
|
|
if self.staff_type in ["accountant","manager"]:
|
|
EntityManagementModel.objects.get_or_create(
|
|
user=self.user,entity=self.dealer.entity
|
|
)
|
|
else:
|
|
self.user.groups.clear()
|
|
group = Group.objects.filter(customgroup__name__iexact=self.staff_type).first()
|
|
if group:
|
|
self.add_group(group)
|
|
class Meta:
|
|
verbose_name = _("Staff")
|
|
verbose_name_plural = _("Staff")
|
|
permissions = []
|
|
|
|
def __str__(self):
|
|
return f"{self.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")
|
|
PENDING = "pending", _("Pending")
|
|
IN_PROGRESS = "in_progress", _("In Progress")
|
|
QUALIFIED = "qualified", _("Qualified")
|
|
CONTACTED = "contacted", _("Contacted")
|
|
CONVERTED = "converted", _("Converted")
|
|
CANCELED = "canceled", _("Canceled")
|
|
|
|
|
|
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")
|
|
WHATSAPP = "whatsapp", _("WhatsApp")
|
|
VISIT = "visit", _("Visit")
|
|
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):
|
|
PROSPECT = "prospect", _("Prospect")
|
|
PROPOSAL = "proposal", _("Proposal")
|
|
NEGOTIATION = "negotiation", _("Negotiation")
|
|
CLOSED_WON = "closed_won", _("Closed Won")
|
|
CLOSED_LOST = "closed_lost", _("Closed Lost")
|
|
|
|
|
|
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"
|
|
)
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
|
|
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 = models.DateField(verbose_name=_("Date of Birth"))
|
|
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
|
national_id = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("National ID")
|
|
)
|
|
phone_number = PhoneNumberField(
|
|
region="SA", unique=True, verbose_name=_("Phone Number")
|
|
)
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Customer")
|
|
verbose_name_plural = _("Customers")
|
|
|
|
def __str__(self):
|
|
middle = f" {self.middle_name}" if self.middle_name else ""
|
|
return f"{self.first_name}{middle} {self.last_name}"
|
|
|
|
@property
|
|
def get_full_name(self):
|
|
return f"{self.first_name} {self.middle_name} {self.last_name}"
|
|
|
|
|
|
class Organization(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(
|
|
Dealer, on_delete=models.CASCADE, related_name="organizations"
|
|
)
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
crn = models.CharField(
|
|
max_length=15, verbose_name=_("Commercial Registration Number")
|
|
)
|
|
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
|
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
logo = models.ImageField(
|
|
upload_to="logos", blank=True, null=True, verbose_name=_("Logo")
|
|
)
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Organization")
|
|
verbose_name_plural = _("Organizations")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Representative(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(
|
|
Dealer, on_delete=models.CASCADE, related_name="representatives"
|
|
)
|
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
|
id_number = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("ID Number")
|
|
)
|
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
|
|
address = models.CharField(
|
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
|
)
|
|
organization = models.ManyToManyField(Organization, related_name="representatives")
|
|
|
|
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 = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
lead_type = models.CharField(
|
|
max_length=50, choices=[("customer", _("Customer")), ("organization", _("Organization"))], verbose_name=_("Lead Type")
|
|
,default="customer")
|
|
customer = models.ForeignKey(
|
|
CustomerModel, on_delete=models.CASCADE, related_name="leads",
|
|
null=True,blank=True
|
|
)
|
|
# car = models.ForeignKey(
|
|
# Car, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Car")
|
|
# )
|
|
id_car_make = models.ForeignKey(
|
|
CarMake,
|
|
on_delete=models.DO_NOTHING,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Make"),
|
|
)
|
|
id_car_model = models.ForeignKey(
|
|
CarModel,
|
|
on_delete=models.DO_NOTHING,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name=_("Model"),
|
|
)
|
|
year = models.PositiveSmallIntegerField(
|
|
verbose_name=_("Year"), blank=True, null=True
|
|
)
|
|
source = models.CharField(
|
|
max_length=50, choices=Sources.choices, verbose_name=_("Source")
|
|
)
|
|
channel = models.CharField(
|
|
max_length=50, choices=Channel.choices, verbose_name=_("Channel")
|
|
)
|
|
crn = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("Commercial Registration Number"), blank=True, null=True
|
|
)
|
|
vrn = models.CharField(
|
|
max_length=15, unique=True, verbose_name=_("VAT Registration Number"), blank=True, null=True
|
|
)
|
|
address = models.CharField(max_length=50, verbose_name=_("address"))
|
|
staff = models.ForeignKey(
|
|
Staff,
|
|
on_delete=models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
related_name="assigned",
|
|
verbose_name=_("Assigned"),
|
|
)
|
|
priority = models.CharField(
|
|
max_length=10,
|
|
choices=Priority.choices,
|
|
default=Priority.MEDIUM,
|
|
verbose_name=_("Priority"),
|
|
)
|
|
status = models.CharField(
|
|
max_length=50,
|
|
choices=Status.choices,
|
|
verbose_name=_("Status"),
|
|
db_index=True,
|
|
default=Status.NEW,
|
|
)
|
|
created = models.DateTimeField(
|
|
auto_now_add=True, verbose_name=_("Created"), db_index=True
|
|
)
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Lead")
|
|
verbose_name_plural = _("Leads")
|
|
|
|
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 is_converted(self):
|
|
return bool(self.customer)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"first_name": str(self.first_name),
|
|
"last_name": str(self.last_name),
|
|
"email": str(self.email),
|
|
"address": str(self.address),
|
|
"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,entity,lead):
|
|
customer = entity.get_customers().filter(email=self.email).first()
|
|
if entity and not customer:
|
|
customer = entity.create_customer(
|
|
commit=False,
|
|
customer_model_kwargs={
|
|
"customer_name": self.full_name,
|
|
"address_1": self.address,
|
|
"phone": self.phone_number,
|
|
"email": self.email,
|
|
}
|
|
)
|
|
|
|
customer.additional_info.update({"info":self.to_dict()})
|
|
if lead.lead_type == "organization":
|
|
customer.additional_info.update({"type":"organization"})
|
|
else:
|
|
customer.additional_info.update({"type":"customer"})
|
|
customer.save()
|
|
self.customer = customer
|
|
self.status = Status.QUALIFIED
|
|
self.save()
|
|
return customer
|
|
|
|
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.id)
|
|
def get_notes(self):
|
|
return Notes.objects.filter(content_type__model="lead", object_id=self.id)
|
|
def get_activities(self):
|
|
return Activity.objects.filter(content_type__model="lead", object_id=self.id)
|
|
|
|
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')),
|
|
]
|
|
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='schedules')
|
|
customer = models.ForeignKey(CustomerModel, on_delete=models.CASCADE, related_name='schedules',null=True,blank=True)
|
|
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
|
scheduled_at = models.DateTimeField()
|
|
scheduled_type = models.CharField(max_length=200, choices=ScheduledType,default='Call')
|
|
duration = models.DurationField(default=timedelta(minutes=5))
|
|
notes = models.TextField(blank=True, null=True)
|
|
status = models.CharField(max_length=200, choices=ScheduleStatusChoices, default='Scheduled')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"Scheduled {self.purpose} with {self.lead.full_name} on {self.scheduled_at}"
|
|
|
|
@property
|
|
def schedule_past_date(self):
|
|
if self.scheduled_at < now():
|
|
return True
|
|
return False
|
|
|
|
class Meta:
|
|
ordering = ['-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(
|
|
CustomerModel, on_delete=models.CASCADE, related_name="opportunities"
|
|
)
|
|
car = models.ForeignKey(
|
|
Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")
|
|
)
|
|
stage = models.CharField(
|
|
max_length=20, choices=Stage.choices, verbose_name=_("Stage")
|
|
)
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=Status.choices,
|
|
verbose_name=_("Status"),
|
|
default=Status.NEW,
|
|
)
|
|
staff = models.ForeignKey(
|
|
Staff,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name="owner",
|
|
verbose_name=_("Owner"),
|
|
)
|
|
lead = models.OneToOneField("Lead",related_name="opportunity", on_delete=models.CASCADE,null=True,blank=True)
|
|
probability = models.PositiveIntegerField(validators=[validate_probability])
|
|
closing_date = models.DateField(verbose_name=_("Closing Date"),null=True,blank=True)
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
|
closed = models.BooleanField(default=False, verbose_name=_("Closed"))
|
|
estimate = models.OneToOneField(EstimateModel, related_name="opportunity",on_delete=models.SET_NULL,null=True,blank=True)
|
|
class Meta:
|
|
verbose_name = _("Opportunity")
|
|
verbose_name_plural = _("Opportunities")
|
|
|
|
def __str__(self):
|
|
return f"Opportunity for {self.customer.customer_name}"
|
|
|
|
|
|
class Notes(models.Model):
|
|
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")
|
|
|
|
def __str__(self):
|
|
return f"Note by {self.created_by.first_name} 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")
|
|
|
|
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")
|
|
|
|
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.CharField(max_length=255, verbose_name=_("Message"))
|
|
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
|
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Notification")
|
|
verbose_name_plural = _("Notifications")
|
|
ordering = ["-created"]
|
|
|
|
def __str__(self):
|
|
return self.message
|
|
|
|
|
|
class Vendor(models.Model, LocalizedNameMixin):
|
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors")
|
|
crn = models.CharField(
|
|
max_length=10, unique=True, verbose_name=_("Commercial Registration Number")
|
|
)
|
|
vrn = models.CharField(
|
|
max_length=15, unique=True, verbose_name=_("VAT Registration Number")
|
|
)
|
|
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 = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
|
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")
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Vendor")
|
|
verbose_name_plural = _("Vendors")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
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
|
|
)
|
|
|
|
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"))
|
|
|
|
|
|
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):
|
|
estimate = models.ForeignKey(
|
|
EstimateModel,
|
|
on_delete=models.CASCADE,
|
|
related_name="sale_orders",
|
|
verbose_name=_("Estimate")
|
|
)
|
|
invoice = models.ForeignKey(
|
|
InvoiceModel,
|
|
on_delete=models.CASCADE,
|
|
related_name="sale_orders",
|
|
verbose_name=_("Invoice"),
|
|
null=True,
|
|
blank=True
|
|
)
|
|
payment_method = models.CharField(max_length=20, choices=[
|
|
('cash', _('Cash')),
|
|
('finance', _('Finance')),
|
|
('lease', _('Lease')),
|
|
("credit_card", _("Credit Card")),
|
|
("bank_transfer", _("Bank Transfer")),
|
|
("sadad", _("SADAD")),
|
|
])
|
|
comments = models.TextField(blank=True, null=True)
|
|
formatted_order_id = models.CharField(max_length=10, unique=True, editable=False)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created']
|
|
|
|
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"Sale Order for {self.full_name}"
|
|
|
|
@property
|
|
def full_name(self):
|
|
return f"{self.customer.customer_name}"
|
|
|
|
@property
|
|
def price(self):
|
|
return self.car.finances.selling_price
|
|
|
|
@property
|
|
def items(self):
|
|
if self.estimate.get_itemtxs_data():
|
|
return self.estimate.get_itemtxs_data()[0]
|
|
return []
|
|
|
|
@property
|
|
def customer(self):
|
|
return self.estimate.customer
|
|
|
|
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)
|
|
|
|
@property
|
|
def users(self):
|
|
return self.group.user_set.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 as e:
|
|
pass
|
|
|
|
def set_default_permissions(self):
|
|
self.clear_permissions()
|
|
if self.name == "Manager":
|
|
self.set_permissions(app="inventory",allowed_models=["car","carfinance","carlocation","customcard","cartransfer","carcolors","carequipment","interiorcolors","exteriorcolors","carreservation"])
|
|
self.set_permissions(app="inventory",allowed_models=["lead","customgroup","saleorder","payment","staff","schedule","activity","opportunity"])
|
|
self.set_permissions(app="django_ledger",allowed_models=["estimatemodel","invoicemodel","accountmodel","chartofaccountmodel","customermodel","billmodel"])
|
|
elif self.name == "Inventory":
|
|
self.set_permissions(app="inventory",allowed_models=["car","carequipment","interiorcolors","exteriorcolors","carcolors","carlocation","customcard","carreservation"])
|
|
elif self.name == "Sales":
|
|
self.set_permissions(app="django_ledger",allowed_models=["estimatemodel","invoicemodel","customermodel"])
|
|
|
|
self.set_permissions(app="inventory",allowed_models=["saleorder","payment","staff","schedule","activity","opportunity","customer","organization"])
|
|
self.set_permissions(app="inventory",allowed_models=["lead","salequotation","salequotationcar"],
|
|
other_perms=['view_car','view_carlocation','view_customcard','view_carcolors','view_cartransfer'])
|
|
elif self.name == "Accountant":
|
|
self.set_permissions(app="inventory",allowed_models=["carfinance"],other_perms=['view_car','view_carlocation','view_customcard','view_carcolors','view_cartransfer','view_saleorder'])
|
|
self.set_permissions(app="django_ledger",allowed_models=["bankaccountmodel","accountmodel","chartofaccountmodel","customcard","billmodel","itemmodel","invoicemodel","vendormodel"],other_perms=['view_customermodel','view_estimatemodel'])
|
|
elif self.name == "Agent":
|
|
# Todo : set permissions for agent
|
|
pass
|
|
|
|
|
|
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
|