haikal/inventory/models.py
2025-05-07 14:35:41 +03:00

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