haikal/inventory/models.py
2025-02-10 14:26:32 +00:00

1715 lines
57 KiB
Python

from decimal import Decimal
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 .mixins import LocalizedNameMixin
from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
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 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 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(
VendorModel,
models.DO_NOTHING,
null=True,
blank=True,
related_name="cars",
verbose_name=_("Vendor"),
)
id_car_make = models.ForeignKey(
CarMake,
models.DO_NOTHING,
db_column="id_car_make",
null=True,
blank=True,
verbose_name=_("Make"),
)
id_car_model = models.ForeignKey(
CarModel,
models.DO_NOTHING,
db_column="id_car_model",
null=True,
blank=True,
verbose_name=_("Model"),
)
year = models.IntegerField(verbose_name=_("Year"))
id_car_serie = models.ForeignKey(
CarSerie,
models.DO_NOTHING,
db_column="id_car_serie",
null=True,
blank=True,
verbose_name=_("Series"),
)
id_car_trim = models.ForeignKey(
CarTrim,
models.DO_NOTHING,
db_column="id_car_trim",
null=True,
blank=True,
verbose_name=_("Trim"),
)
status = models.CharField(
max_length=10,
choices=CarStatusChoices.choices,
default=CarStatusChoices.AVAILABLE,
verbose_name=_("Status"),
)
stock_type = models.CharField(
max_length=10,
choices=CarStockTypeChoices.choices,
default=CarStockTypeChoices.NEW,
verbose_name=_("Stock Type"),
)
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
hash = models.CharField(max_length=64, blank=True, null=True, verbose_name=_("Hash"))
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 is_reserved(self):
active_reservations = self.reservations.filter(reserved_until__gt=now())
return active_reservations.exists()
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.first().exterior.name if self.colors.exists() else ""
except:
pass
hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}{self.year}{self.id_car_serie.name}{self.id_car_trim.name}{color}".encode('utf-8'))
return hash_object.hexdigest()
def mark_as_sold(self):
self.cancel_reservation()
self.status = CarStatusChoices.SOLD
self.save()
def cancel_reservation(self):
if self.reservations.exists():
self.reservations.all().delete()
def cancel_transfer(self):
if self.transfer_logs.exists():
self.transfer_logs.all().delete()
def to_dict(self):
return {
"vin": self.vin,
"make": self.id_car_make.name if self.id_car_make else "Unknown Make",
"model": self.id_car_model.name if self.id_car_model else "Unknown Model",
"trim": self.id_car_trim.name if self.id_car_trim else "Unknown Trim",
"year": self.year,
"display_name": self.get_car_group,
"status": self.status,
"stock_type": self.stock_type,
"remarks": self.remarks,
"mileage": self.mileage,
"receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'),
'hash': self.get_hash,
"id": self.id,
}
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_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(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.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors")
exterior = models.ForeignKey(
"ExteriorColors", on_delete=models.CASCADE, related_name="colors"
)
interior = models.ForeignKey(
"InteriorColors", on_delete=models.CASCADE, related_name="colors"
)
class Meta:
verbose_name = _("Color")
verbose_name_plural = _("Colors")
unique_together = ("car", "exterior", "interior")
def __str__(self):
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
class CustomCard(models.Model):
car = models.OneToOneField(
Car,
on_delete=models.CASCADE,
related_name="custom_cards",
verbose_name=_("Car"),
)
custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number"))
custom_date = models.DateField(verbose_name=_("Custom Date"))
class Meta:
verbose_name = _("Custom Card")
verbose_name_plural = _("Custom Cards")
def __str__(self):
return f"{self.car} - {self.custom_number}"
class CarLocation(models.Model):
car = models.OneToOneField(
Car, on_delete=models.CASCADE, related_name="location", verbose_name=_("Car")
)
owner = models.ForeignKey(
"Dealer",
on_delete=models.CASCADE,
related_name="owned_cars",
verbose_name=_("Owner"),
help_text=_("Dealer who owns the car."),
)
showroom = models.ForeignKey(
"Dealer",
on_delete=models.CASCADE,
related_name="showroom_cars",
verbose_name=_("Showroom"),
help_text=_("Dealer where the car is displayed (can be the owner)."),
)
description = models.TextField(
blank=True,
null=True,
verbose_name=_("Description"),
help_text=_("Optional description about the showroom placement."),
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Last Updated"))
class Meta:
verbose_name = _("Car Location")
verbose_name_plural = _("Car Locations")
def __str__(self):
return f"Car: {self.car}, Showroom: {self.showroom}, Owner: {self.owner}"
def is_owner_showroom(self):
"""
Returns True if the showroom is the same as the owner.
"""
return self.owner == self.showroom
class CarRegistration(models.Model):
car = models.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 Subscription(models.Model):
plan = models.ForeignKey(
"SubscriptionPlan", on_delete=models.CASCADE, related_name="subscriptions"
)
start_date = models.DateField(help_text="Date when the subscription starts")
end_date = models.DateField(help_text="Date when the subscription ends")
users = models.ManyToManyField(
User, through="SubscriptionUser"
) # many-to-many relationship with User model
is_active = models.BooleanField(default=True)
billing_cycle = models.CharField(
max_length=10,
choices=[("monthly", "Monthly"), ("annual", "Annual")],
default="monthly",
help_text="Billing cycle for the subscription",
)
last_payment_date = models.DateField(
null=True, blank=True, help_text="Date of the last payment made"
)
next_payment_date = models.DateField(
null=True, blank=True, help_text="Date of the next payment due"
)
class Meta:
verbose_name = _("Subscription")
verbose_name_plural = _("Subscriptions")
def __str__(self):
return self.plan.name
@property
def total_subscribers(self):
return self.users.count()
class SubscriptionUser(models.Model):
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
verbose_name = _("Subscription User")
verbose_name_plural = _("Subscription Users")
def __str__(self):
return f"{self.subscription} - {self.user}"
class SubscriptionPlan(models.Model):
name = models.CharField(
max_length=100, unique=True, help_text=_("Name of the subscription plan")
)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
max_users = models.PositiveIntegerField(
help_text=_("Maximum number of users allowed"), default=1
)
max_inventory_size = models.PositiveIntegerField(
help_text=_("Maximum number of cars in inventory"), default=50
)
support_level = models.CharField(
max_length=50,
choices=[
("basic", "Basic Support"),
("priority", "Priority Support"),
("dedicated", "Dedicated Support"),
],
default="basic",
help_text="Level of support provided",
)
custom_features = models.JSONField(
blank=True, null=True, help_text=_("Additional features specific to this plan")
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("Subscription Plan")
verbose_name_plural = _("Subscription Plans")
def __str__(self):
return f"{self.name} - {self.price}"
class Dealer(models.Model, LocalizedNameMixin):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer")
crn = models.CharField(
max_length=10,
verbose_name=_("Commercial Registration Number"),
null=True,
blank=True,
)
vrn = models.CharField(
max_length=15, verbose_name=_("VAT Registration Number"), null=True, blank=True
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")
)
entity = models.ForeignKey(
EntityModel, on_delete=models.SET_NULL, null=True, blank=True
)
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
objects = DealerUserManager()
@property
def get_active_plan(self):
try:
return self.user.subscription_set.filter(is_active=True).first()
except SubscriptionPlan.DoesNotExist:
return None
@property
def get_plan(self):
active_plan = self.get_active_plan
if active_plan:
subscription_plan = SubscriptionPlan.objects.filter(
pk=active_plan.pk
).first()
if subscription_plan:
return subscription_plan
return None
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
# permissions = [
# ('change_dealer_type', 'Can change dealer type'),
# ]
def __str__(self):
return self.name
# @property
# def get_sub_dealers(self):
# if self.dealer_type == "OWNER":
# return self.sub_dealers.all()
# return None
#
# @property
# def is_parent(self):
# return self.dealer_type == "OWNER"
# @property
# def get_root_dealer(self):
# return self.parent_dealer if self.parent_dealer else self
##############################
# Additional staff types for later
# COORDINATOR = "coordinator", _("Coordinator")
# RECEPTIONIST = "receptionist", _("Receptionist")
# AGENT = "agent", _("Agent")
# TECHNICIAN = "technician", _("Technician")
# DRIVER = "driver", _("Driver")
##############################
class StaffTypes(models.TextChoices):
MANAGER = "manager", _("Manager")
INVENTORY = "inventory", _("Inventory")
ACCOUNTANT = "accountant", _("Accountant")
SALES = "sales", _("Sales")
COORDINATOR = "coordinator", _("Coordinator")
RECEPTIONIST = "receptionist", _("Receptionist")
AGENT = "agent", _("Agent")
class Staff(models.Model, LocalizedNameMixin):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
staff_type = models.CharField(
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
objects = StaffUserManager()
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")
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")
RESERVE_CAR = "reserve_car", _("Reserve 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"
)
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"))
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")
)
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}"
@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):
customer = entity.get_customers().filter(email=self.email).first()
if entity and not customer:
customer = entity.create_customer(
customer_model_kwargs={
"customer_name": self.full_name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
}
)
customer.additional_info = {}
customer.additional_info.update({"info":self.to_dict()})
self.customer = customer
customer.save()
self.save()
def get_latest_schedule(self):
return self.schedules.order_by('-scheduled_at').first()
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(Staff, 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"),
)
probability = models.PositiveIntegerField(validators=[validate_probability])
closing_date = models.DateField(verbose_name=_("Closing Date"))
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"))
class Meta:
verbose_name = _("Opportunity")
verbose_name_plural = _("Opportunities")
def __str__(self):
return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer}"
class Notes(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
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 Activity(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
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"
)
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")
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")
def __str__(self):
return self.name
# class SaleQuotation(models.Model):
# quotation_number = models.CharField(max_length=10, unique=True)
#
# STATUS_CHOICES = [
# ("Draft", _("Draft")),
# ("Approved", _("Approved")),
# ("In Review", _("In Review")),
# ("Paid", _("Paid")),
# ]
# dealer = models.ForeignKey(
# Dealer, on_delete=models.CASCADE, related_name="sales", null=True
# )
# customer = models.ForeignKey(
# Customer,
# on_delete=models.CASCADE,
# related_name="quotations",
# verbose_name=_("Customer"),
# )
# amount = models.DecimalField(
# decimal_places=2,
# default=Decimal("0.00"),
# max_digits=10,
# verbose_name=_("Amount"),
# )
# remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
# is_approved = models.BooleanField(default=False)
# status = models.CharField(
# max_length=10, choices=STATUS_CHOICES, default="Draft", verbose_name=_("Status")
# )
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
# updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
#
# posted = models.BooleanField(default=False)
# payment_id = models.CharField(
# max_length=255, null=True, blank=True, verbose_name=_("Payment ID")
# )
# is_paid = models.BooleanField(default=False)
# date_draft = models.DateTimeField(
# null=True, blank=True, verbose_name=_("Draft Date")
# )
# date_in_review = models.DateTimeField(
# null=True, blank=True, verbose_name=_("In Review Date")
# )
# date_approved = models.DateTimeField(
# null=True, blank=True, verbose_name=_("Approved Date")
# )
# date_paid = models.DateTimeField(null=True, blank=True, verbose_name=_("Paid Date"))
# date_void = models.DateTimeField(null=True, blank=True, verbose_name=_("Void Date"))
# date_canceled = models.DateTimeField(
# null=True, blank=True, verbose_name=_("Canceled Date")
# )
#
# @property
# def total_quantity(self):
# total_quantity = self.quotation_cars.aggregate(total=Sum("quantity"))["total"]
# return total_quantity or 0
#
# @property
# def total(self):
# total = self.quotation_cars.aggregate(
# total_price=Sum(F("car__finances__selling_price") * F("quantity"))
# )
# if not total:
# return 0
# return total["total_price"]
#
# @property
# def total_vat(self):
# if self.total:
# return float(self.total) * 0.15 + float(self.total)
# return 0
# def confirm(self):
# """Confirm the quotation and lock financial details."""
# if self.status != "DRAFT":
# raise ValueError(_("Only draft quotations can be confirmed."))
# self.status = "CONFIRMED"
# self.save()
# def cancel(self):
# """Cancel the quotation."""
# if self.status == "CONFIRMED":
# raise ValueError(_("Cannot cancel a confirmed quotation."))
# self.status = "CANCELED"
# self.save()
# def __str__(self):
# return f"Quotation #{self.quotation_number} for {self.customer}"
#
# @property
# def display_quotation_number(self):
# return f"QN-{self.quotation_number}"
#
# def save(self, *args, **kwargs):
# if not self.quotation_number:
# self.quotation_number = str(next(self._get_quotation_number())).zfill(6)
# super().save(*args, **kwargs)
#
# @classmethod
# def _get_quotation_number(cls):
# last_quotation = cls.objects.all().order_by("id").last()
# if last_quotation:
# last_quotation_number = int(last_quotation.quotation_number)
# else:
# last_quotation_number = 0
# return itertools.count(last_quotation_number + 1)
#
# class SaleQuotationCar(models.Model):
# quotation = models.ForeignKey(
# SaleQuotation,
# on_delete=models.CASCADE,
# related_name="quotation_cars",
# verbose_name=_("Quotation"),
# )
# car = models.ForeignKey(Car, on_delete=models.CASCADE, verbose_name=_("Car"))
# quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
#
# @property
# def finance(self):
# return self.car.finances
#
# @property
# def financial_details(self):
# """
# Retrieve financial details dynamically from CarFinance.
# Returns a dictionary with all financial fields for better access.
# """
# car_finance = self.car.finances
# if not car_finance:
# return None
#
# return {
# "selling_price": car_finance.selling_price,
# "administration_fee": car_finance.administration_fee,
# "transportation_fee": car_finance.transportation_fee,
# "custom_card_fee": car_finance.custom_card_fee,
# "registration_fee": car_finance.registration_fee,
# "vat_amount": car_finance.vat_amount,
# # "total_amount": car_finance.total,
# }
#
# @property
# def total(self):
# """
# Calculate total price dynamically based on quantity and selling price.
# """
# if not self.car.finances:
# return Decimal("0.00")
# return self.car.finances.selling_price * self.quantity
#
# @property
# def total_vat(self):
# """
# Calculate total price dynamically based on quantity and selling price.
# """
# if not self.car.finances:
# return Decimal("0.00")
# price = float(self.car.finances.selling_price * self.quantity)
# return (price * 0.15) + price
#
# def __str__(self):
# return f"{self.car} - Quotation #{self.quotation.id}"
#
#
# class SalesOrder(models.Model):
# quotation = models.OneToOneField(
# SaleQuotation,
# on_delete=models.CASCADE,
# related_name="sales_order",
# verbose_name=_("Quotation"),
# )
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
# total_amount = models.DecimalField(
# max_digits=14, decimal_places=2, verbose_name=_("Total Amount")
# )
#
# def __str__(self):
# return f"Sales Order #{self.id} from Quotation #{self.quotation.id}"
class Payment(models.Model):
METHOD_CHOICES = [
("cash", _("cash")),
("credit", _("credit")),
("transfer", _("transfer")),
("debit", _("debit")),
("SADAD", _("SADAD")),
]
# quotation = models.ForeignKey(
# SaleQuotation, on_delete=models.CASCADE, related_name="payments"
# )
amount = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("amount")
)
payment_method = models.CharField(
choices=METHOD_CHOICES, max_length=50, verbose_name=_("method")
)
reference_number = models.CharField(
max_length=100, null=True, blank=True, verbose_name=_("reference number")
)
payment_date = models.DateField(auto_now_add=True, verbose_name=_("date"))
# def save(self, *args, **kwargs):
# super().save(*args, **kwargs)
# self.quotation.remaining_balance -= self.amount
# if self.quotation.remaining_balance <= 0:
# self.quotation.is_paid = True
# self.quotation.save()
class Meta:
verbose_name = _("payment")
verbose_name_plural = _("payments")
def __str__(self):
return f"Payment of {self.amount} on {self.payment_date} for {self.quotation}"
class Refund(models.Model):
payment = models.OneToOneField(
Payment, on_delete=models.CASCADE, related_name="refund"
)
amount = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("amount")
)
reason = models.TextField(blank=True, verbose_name=_("reason"))
refund_date = models.DateField(auto_now_add=True, verbose_name=_("refund date"))
class Meta:
verbose_name = _("refund")
verbose_name_plural = _("refunds")
def __str__(self):
return f"Refund of {self.amount} on {self.refund_date}"
class UserActivityLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "User Activity Log"
verbose_name_plural = "User Activity Logs"
ordering = ["-timestamp"]
def __str__(self):
return f"{self.user.email} - {self.action} - {self.timestamp}"
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'),
])
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