This commit is contained in:
Marwan Alwali 2025-01-22 16:02:43 +03:00
parent d675d50ae6
commit 8df5fbef8c

View File

@ -17,7 +17,7 @@ from django_ledger.models import (
UnitOfMeasureModel,
CustomerModel,
ItemModelQuerySet,
)
from django.db.models import Sum
from decimal import Decimal, InvalidOperation
@ -29,7 +29,7 @@ from sqlalchemy.orm.base import object_state
from .utilities.financials import get_financial_value, get_total, get_total_financials
from django.db.models import FloatField
from .mixins import LocalizedNameMixin
from django_ledger.models import EntityModel,ItemModel
from django_ledger.models import EntityModel, ItemModel
from django_countries.fields import CountryField
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@ -38,32 +38,35 @@ 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)
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)
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'
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):
@ -74,6 +77,7 @@ class VatRate(models.Model):
def __str__(self):
return f"Rate: {self.rate}%"
class CarType(models.IntegerChoices):
CAR = 1, _('Car')
LIGHT_COMMERCIAL = 2, _('Light Commercial')
@ -90,7 +94,6 @@ class CarType(models.IntegerChoices):
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)
@ -159,6 +162,7 @@ class CarEquipment(models.Model, LocalizedNameMixin):
def __str__(self):
return self.name
class Meta:
verbose_name = "Equipment"
@ -239,7 +243,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
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)
item = models.OneToOneField(ItemModel, on_delete=models.CASCADE, verbose_name=_("Item"), null=True, blank=True)
class Meta:
verbose_name = _("Additional Services")
@ -330,9 +334,11 @@ class Car(models.Model):
def get_car_group(self):
return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}"
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_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"))
@ -349,29 +355,29 @@ class CarReservation(models.Model):
# Car Finance Model
class CarFinance(models.Model):
additional_services = models.ManyToManyField(AdditionalServices, related_name="additional_finances",blank=True)
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):
if self.additional_services.count() > 0:
return self.selling_price + sum(x.price for x in self.additional_services.all())
return self.selling_price
@property
def total_discount(self):
if self.discount_amount > 0:
return self.total - self.discount_amount
return self.total
@property
def total_vat(self):
return self.total_discount + self.vat_amount
@property
def vat_amount(self):
vat = VatRate.objects.filter(is_active=True).first()
@ -381,13 +387,12 @@ class CarFinance(models.Model):
@property
def revenue(self):
return self.selling_price-self.cost_price
return self.selling_price - self.cost_price
def __str__(self):
return f"Car: {self.car}, Selling Price: {self.selling_price}"
# def save(self, *args, **kwargs):
# def save(self, *args, **kwargs):
# self.full_clean()
# try:
# price_after_discount = self.selling_price - self.discount_amount
@ -555,6 +560,7 @@ class Subscription(models.Model):
)
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")
@ -607,8 +613,8 @@ 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)
, null=True
, blank=True)
vrn = models.CharField(max_length=15,
verbose_name=_("VAT Registration Number"),
null=True,
@ -650,14 +656,14 @@ class Dealer(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
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":
@ -671,18 +677,18 @@ class Dealer(models.Model, LocalizedNameMixin):
# 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")
# 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")
@ -727,6 +733,7 @@ class Sources(models.TextChoices):
YOUTUBE = "youtube", _("Youtube")
CAMPAIGN = "campaign", _("Campaign")
class Channel(models.TextChoices):
WALK_IN = "walk_in", _("Walk In")
TOLL_FREE = "toll_free", _("Toll Free")
@ -742,6 +749,7 @@ class Status(models.TextChoices):
QUALIFIED = "qualified", _("Qualified")
CANCELED = "canceled", _("Canceled")
class Title(models.TextChoices):
MR = "mr", _("Mr")
MRS = "mrs", _("Mrs")
@ -754,6 +762,7 @@ class Title(models.TextChoices):
COMPANY = "company", _("Company")
NA = "na", _("N/A")
class ActionChoices(models.TextChoices):
CALL = "call", _("Call")
SMS = "sms", _("SMS")
@ -770,6 +779,7 @@ class ActionChoices(models.TextChoices):
CREATE_INVOICE = "create_invoice", _("Create Invoice")
CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice")
class Stage(models.TextChoices):
PROSPECT = "prospect", _("Prospect")
PROPOSAL = "proposal", _("Proposal")
@ -777,6 +787,7 @@ class Stage(models.TextChoices):
CLOSED_WON = "closed_won", _("Closed Won")
CLOSED_LOST = "closed_lost", _("Closed Lost")
class Priority(models.TextChoices):
LOW = "low", _("Low")
MEDIUM = "medium", _("Medium")
@ -784,7 +795,7 @@ class Priority(models.TextChoices):
class Customer(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE,related_name="customers")
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"))
@ -826,6 +837,7 @@ class Organization(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Organization")
verbose_name_plural = _("Organizations")
def __str__(self):
return self.name
@ -852,15 +864,18 @@ class Lead(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads")
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="leads")
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"))
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"))
city = models.CharField(max_length=50, verbose_name=_("City"))
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned"))
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)
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"))
@ -898,7 +913,8 @@ class Opportunity(models.Model):
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"))
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"))
@ -992,7 +1008,6 @@ class Vendor(models.Model, LocalizedNameMixin):
return self.name
class SaleQuotation(models.Model):
quotation_number = models.CharField(max_length=10, unique=True)
@ -1004,7 +1019,7 @@ class SaleQuotation(models.Model):
]
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sales", null=True
)
)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
@ -1024,7 +1039,7 @@ class SaleQuotation(models.Model):
)
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)
@ -1046,13 +1061,13 @@ class SaleQuotation(models.Model):
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":
@ -1069,11 +1084,11 @@ class SaleQuotation(models.Model):
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)
@ -1102,10 +1117,11 @@ class SaleQuotationCar(models.Model):
verbose_name=_("Car")
)
quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
@property
def finance(self):
return self.car.finances
@property
def financial_details(self):
"""
@ -1134,6 +1150,7 @@ class SaleQuotationCar(models.Model):
if not self.car.finances:
return Decimal("0.00")
return self.car.finances.selling_price * self.quantity
@property
def total_vat(self):
"""