haikal/inventory/models.py
2024-12-17 13:33:59 +00:00

681 lines
22 KiB
Python

from uuid import uuid4
from django.db import models, transaction
from django.contrib.auth.models import User
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django_ledger.models import (
VendorModel,
EntityModel,
EntityUnitModel,
ItemModel,
AccountModel,
ItemModelAbstract,
UnitOfMeasureModel,
CustomerModel,
ItemModelQuerySet,
)
from decimal import Decimal, InvalidOperation
from django.core.exceptions import ValidationError
from phonenumber_field.modelfields import PhoneNumberField
from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
from .mixins import LocalizedNameMixin
class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
is_sa_import = models.BooleanField(default=False)
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)
arabic_name = models.CharField(max_length=255)
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)
arabic_name = models.CharField(max_length=255)
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)
arabic_name = models.CharField(max_length=255)
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 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"
# Car Model
class CarStatusChoices(models.TextChoices):
AVAILABLE = "available", _("Available")
SOLD = "sold", _("Sold")
HOLD = "hold", _("Hold")
DAMAGED = "damaged", _("Damaged")
RESERVED = "reserved", _("Reserved")
class CarStockTypeChoices(models.TextChoices):
NEW = "new", _("New")
USED = "used", _("Used")
class DEALER_TYPES(models.TextChoices):
Owner = "Owner", _("Owner")
Inventory = "Inventory", _("Inventory")
Accountent = "Accountent", _("Accountent")
Sales = "sales", _("Sales")
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,
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"))
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()
@property
def selling_price(self):
finance = self.finances.first()
return finance.selling_price if finance else Decimal("0.00")
@property
def discount_amount(self):
finance = self.finances.first()
return finance.discount_amount if finance else Decimal("0.00")
@property
def vat_amount(self):
finance = self.finances.first()
return finance.vat_amount if finance else Decimal("0.00")
@property
def total(self):
finance = self.finances.first()
return finance.total if finance else Decimal("0.00")
class CarReservation(models.Model):
car = models.ForeignKey(
"Car", on_delete=models.CASCADE, related_name="reservations"
)
reserved_by = models.ForeignKey(User, on_delete=models.CASCADE)
reserved_at = models.DateTimeField(auto_now_add=True)
reserved_until = models.DateTimeField()
def is_active(self):
return self.reserved_until > now()
class Meta:
unique_together = ("car", "reserved_until")
ordering = ["-reserved_at"]
# Car Finance Model
class CarFinance(models.Model):
car = models.ForeignKey(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")
)
profit_margin = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Profit Margin"), editable=False
)
vat_amount = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Vat Amount"), editable=False
)
discount_amount = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Discount Amount"),
default=Decimal("0.00"),
)
registration_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Registration Fee"),
default=Decimal("0.00"),
)
administration_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Administration Fee"),
default=Decimal("0.00"),
)
transportation_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Transportation Fee"),
default=Decimal("0.00"),
)
custom_card_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Custom Card Fee"),
default=Decimal("0.00"),
)
vat_rate = models.DecimalField(
max_digits=14,
decimal_places=2,
default=Decimal("0.15"),
verbose_name=_("VAT Rate"),
)
total = models.DecimalField(
max_digits=14, decimal_places=2, default=Decimal("0.00"), null=True, blank=True
)
def __str__(self):
return f"{self.selling_price}"
def save(self, *args, **kwargs):
self.full_clean()
try:
services = (
self.administration_fee + self.transportation_fee + self.custom_card_fee
)
price_after_discount = self.selling_price - self.discount_amount
total_vat_amount = (price_after_discount + services) * self.vat_rate
self.vat_amount = self.selling_price * self.vat_rate
self.profit_margin = (
self.selling_price
- self.cost_price
- self.discount_amount
- self.registration_fee
)
self.total = (
price_after_discount
+ services
+ total_vat_amount
+ self.registration_fee
)
except InvalidOperation as e:
raise ValidationError(_("Invalid decimal operation: %s") % str(e))
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Car Financial Details")
@property
def total_vat_amount(self):
services = (
self.administration_fee + self.transportation_fee + self.custom_card_fee
)
price_after_discount = self.selling_price - self.discount_amount
return (price_after_discount + services) * self.vat_rate
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})"
# Colors Model
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})"
# Custom Card Model
class CustomCard(models.Model):
car = models.ForeignKey(
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}"
# Car Registration Model
class CarRegistration(models.Model):
car = models.ForeignKey(
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"))
text3 = models.CharField(max_length=1, verbose_name=_("Text 3"))
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} - {self.text1} {self.text2} {self.text3}"
# 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
#subscription
class Subscription(models.Model):
plan = models.CharField(max_length=255) # e.g. "basic", "premium"
start_date = models.DateField()
end_date = models.DateField()
max_users = models.IntegerField() # maximum number of users per account
users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model
is_active = models.BooleanField(default=True)
def __str__(self):
return self.plan
class SubscriptionUser(models.Model):
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f"{self.subscription} - {self.user}"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
max_users = models.IntegerField() # maximum number of users per account
def __str__(self):
return f"{self.name} - {self.price}"
# Dealer Model
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")
)
parent_dealer = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("Parent Dealer"),
related_name="sub_dealers",
)
dealer_type = models.CharField(
max_length=255,
choices=DEALER_TYPES.choices,
verbose_name=_("Dealer Type"),
default=DEALER_TYPES.Owner,
)
@property
def get_active_plan(self):
try:
return self.user.subscription_set.filter(is_active=True).first()
except SubscriptionPlan.DoesNotExist:
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_parent_or_self(self):
return self.parent_dealer if self.parent_dealer else self
# Vendor Model
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"))
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")
)
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")
def __str__(self):
return self.name
# Customer Model
class Customer(models.Model):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="customers"
)
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"))
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"))
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 SaleQuotation(models.Model):
STATUS_CHOICES = [
("DRAFT", _("Draft")),
("CONFIRMED", _("Confirmed")),
("CANCELED", _("Canceled")),
]
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"))
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"))
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.id} for {self.customer}"
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"))
# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="sale_cars", verbose_name=_("Dealer"))
quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
# price = models.DecimalField(decimal_places=2, max_digits=10, verbose_name=_("Price"), editable=False)
def get_financial_details(self):
"""Retrieve financial details dynamically from CarFinance."""
car_finance = self.car.finances.first()
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_rate": car_finance.vat_rate,
"discount_amount": car_finance.discount_amount,
"total_amount": car_finance.total,
}
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}"