504 lines
18 KiB
Python
504 lines
18 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 phonenumber_field.modelfields import PhoneNumberField
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from decimal import Decimal
|
|
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')
|
|
|
|
|
|
class CarStockTypeChoices(models.TextChoices):
|
|
NEW = 'new', _('New')
|
|
USED = 'used', _('Used')
|
|
|
|
|
|
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 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"))
|
|
profit_margin = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Profit Margin"))
|
|
selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price"), editable=False)
|
|
vat_rate = models.DecimalField(max_digits=10, decimal_places=2, default=0.15, verbose_name=_("VAT Rate"))
|
|
vat_amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name=_("VAT Amount"), editable=False)
|
|
total = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Total Amount"), editable=False)
|
|
|
|
class Meta:
|
|
verbose_name = _("Car Financial Details")
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.full_clean()
|
|
self.selling_price = self.cost_price * (1 + self.profit_margin)
|
|
self.vat_amount = self.selling_price * self.vat_rate
|
|
self.total = self.selling_price + self.vat_amount
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return f"Car Financial Details for {self.car}: Selling Price {self.selling_price}"
|
|
|
|
|
|
# Colors Model
|
|
class CarColors(models.Model):
|
|
class ColorType(models.TextChoices):
|
|
EXTERIOR = 'exterior', _("Exterior")
|
|
INTERIOR = 'interior', _("Interior")
|
|
|
|
car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='colors')
|
|
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"))
|
|
color_type = models.CharField(
|
|
max_length=10,
|
|
choices=ColorType.choices,
|
|
default=ColorType.EXTERIOR,
|
|
verbose_name=_("Color Type")
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Color")
|
|
verbose_name_plural = _("Colors")
|
|
|
|
def __str__(self):
|
|
return f"{self.get_color_type_display()} - {self.name} ({self.rgb})"
|
|
|
|
|
|
# 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
|
|
|
|
|
|
# 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"))
|
|
vrn = models.CharField(max_length=15, 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"))
|
|
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"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Dealer")
|
|
verbose_name_plural = _("Dealers")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
# 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"))
|
|
|
|
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}"
|
|
|
|
|
|
# Create Entity
|
|
@receiver(post_save, sender=Dealer)
|
|
def create_ledger_entity(sender, instance, created, **kwargs):
|
|
if created:
|
|
entity = EntityModel.objects.create(
|
|
name=instance.name,
|
|
admin=instance.user,
|
|
address_1=instance.address,
|
|
fy_start_month=1,
|
|
accrual_method=True,
|
|
depth=0,
|
|
)
|
|
|
|
default_coa = entity.create_chart_of_accounts(assign_as_default=True,
|
|
commit=True,
|
|
coa_name=_("Chart of Accounts"))
|
|
if default_coa:
|
|
entity.populate_default_coa(activate_accounts=True, coa_model=default_coa)
|
|
print(f"Ledger entity created for Dealer: {instance.name}")
|
|
|
|
|
|
# # Create Vendor
|
|
@receiver(post_save, sender=Vendor)
|
|
def create_ledger_vendor(sender, instance, created, **kwargs):
|
|
|
|
if created:
|
|
entity = EntityModel.objects.filter(name=instance.dealer.name).first()
|
|
|
|
vendor = VendorModel.objects.update_or_create(
|
|
entity_model=entity,
|
|
vendor_name=instance.name,
|
|
vendor_number=instance.crn,
|
|
address_1=instance.address,
|
|
phone=instance.phone_number,
|
|
tax_id_number=instance.vrn,
|
|
active=True,
|
|
hidden=False,
|
|
additional_info={
|
|
"arabic_name": instance.arabic_name,
|
|
"contact_person": instance.contact_person,
|
|
},
|
|
)
|
|
|
|
print(f"VendorModel created for Vendor: {instance.name}")
|
|
|
|
|
|
@receiver(post_save, sender=Customer)
|
|
def create_customer(sender, instance, created, **kwargs):
|
|
|
|
if created:
|
|
entity = EntityModel.objects.filter(name=instance.dealer.name).first()
|
|
name = f"{instance.first_name} {instance.middle_name} {instance.last_name}"
|
|
|
|
customer = CustomerModel.objects.create(
|
|
entity_model=entity,
|
|
customer_name=name,
|
|
customer_number=instance.national_id,
|
|
address_1=instance.address,
|
|
phone=instance.phone_number,
|
|
email=instance.email,
|
|
sales_tax_rate=0.15,
|
|
)
|
|
|
|
print(f"Customer created: {name}")
|
|
|
|
|
|
# # Create Item
|
|
# @receiver(post_save, sender=Car)
|
|
# def create_item_model(sender, instance, created, **kwargs):
|
|
# item_name = f"{instance.year} - {instance.id_car_make} - {instance.id_car_model} - {instance.id_car_trim}"
|
|
# uom_name = _("Car")
|
|
# unit_abbr = _("C")
|
|
#
|
|
# uom, uom_created = UnitOfMeasureModel.objects.get_or_create(
|
|
# name=uom_name,
|
|
# unit_abbr=unit_abbr
|
|
# )
|
|
#
|
|
# if uom_created:
|
|
# print(f"UOM created: {uom_name}")
|
|
# else:
|
|
# print(f"Using existing UOM: {uom_name}")
|
|
#
|
|
# entity = EntityModel.objects.filter(name=instance.dealer.name).first()
|
|
#
|
|
# inventory_account = AccountModel.objects.first()
|
|
# cogs_account = AccountModel.objects.first()
|
|
# earnings_account = AccountModel.objects.first()
|
|
#
|
|
# item = ItemModel.objects.create(
|
|
# entity=entity,
|
|
# uom=uom,
|
|
# name=item_name,
|
|
# item_role=ItemModelAbstract.ITEM_ROLE_INVENTORY,
|
|
# item_type=ItemModelAbstract.ITEM_TYPE_MATERIAL,
|
|
# item_id=instance.vin,
|
|
# sold_as_unit=True,
|
|
# inventory_received=1.00,
|
|
# inventory_received_value=0.00,
|
|
# inventory_account=inventory_account,
|
|
# for_inventory=True,
|
|
# is_product_or_service=True,
|
|
# cogs_account=cogs_account,
|
|
# earnings_account=earnings_account,
|
|
# is_active=True,
|
|
# additional_info={
|
|
# "remarks": instance.remarks,
|
|
# "status": instance.status,
|
|
# "stock_type": instance.stock_type,
|
|
# "mileage": instance.mileage,
|
|
# },
|
|
# )
|
|
#
|
|
# print(f"ItemModel {'created' if created else 'updated'} for Car: {item.name}")
|
|
#
|
|
#
|
|
# # update price - CarFinance
|
|
# @receiver(post_save, sender=CarFinance)
|
|
# def update_item_model_cost(sender, instance, created, **kwargs):
|
|
#
|
|
# ItemModel.objects.filter(item_id=instance.car.vin).update(
|
|
# inventory_received_value=instance.cost_price,
|
|
# default_amount=instance.cost_price,
|
|
# )
|
|
# print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|