Merge branch 'main' of http://10.10.1.120:3000/tenhal_admin/haikal
BIN
inventory/.DS_Store
vendored
@ -32,7 +32,7 @@ admin.site.register(models.VatRate)
|
||||
admin.site.register(models.Customer)
|
||||
admin.site.register(models.Opportunity)
|
||||
admin.site.register(models.Notification)
|
||||
admin.site.register(models.OpportunityLog)
|
||||
admin.site.register(models.Lead)
|
||||
|
||||
@admin.register(models.CarMake)
|
||||
class CarMakeAdmin(admin.ModelAdmin):
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from django_countries.widgets import CountrySelectWidget
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.core.validators import RegexValidator
|
||||
@ -25,7 +26,7 @@ from .models import (
|
||||
SaleQuotationCar,
|
||||
AdditionalServices,
|
||||
Staff,
|
||||
Opportunity, DealStatus, Priority, Sources,
|
||||
Opportunity, Priority, Sources,
|
||||
)
|
||||
from django_ledger.models import ItemModel, InvoiceModel
|
||||
from django.forms import ModelMultipleChoiceField, ValidationError
|
||||
@ -61,21 +62,21 @@ class StaffForm(forms.ModelForm):
|
||||
model = Staff
|
||||
fields = ["name", "arabic_name", "phone_number", "staff_type"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user_instance = kwargs.get("instance")
|
||||
if user_instance and user_instance.user:
|
||||
initial = kwargs.setdefault("initial", {})
|
||||
initial["email"] = user_instance.user.email
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, commit=True):
|
||||
staff_instance = super().save(commit=False)
|
||||
user = staff_instance.user
|
||||
user.email = self.cleaned_data["email"]
|
||||
if commit:
|
||||
user.save()
|
||||
staff_instance.save()
|
||||
return staff_instance
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# user_instance = kwargs.get("instance")
|
||||
# if user_instance and user_instance.user:
|
||||
# initial = kwargs.setdefault("initial", {})
|
||||
# initial["email"] = user_instance.user.email
|
||||
# super().__init__(*args, **kwargs)
|
||||
#
|
||||
# def save(self, commit=True):
|
||||
# user_instance = super().save(commit=False)
|
||||
# user = user_instance.user
|
||||
# user.email = self.cleaned_data["email"]
|
||||
# if commit:
|
||||
# user.save()
|
||||
# user_instance.save()
|
||||
# return user_instance
|
||||
|
||||
|
||||
# Dealer Form
|
||||
@ -105,7 +106,9 @@ class CustomerForm(forms.ModelForm, AddClassMixin):
|
||||
"national_id",
|
||||
"phone_number",
|
||||
"address",
|
||||
"country"
|
||||
]
|
||||
widgets = {"country": CountrySelectWidget()}
|
||||
|
||||
|
||||
class CarForm(
|
||||
@ -559,6 +562,6 @@ class OpportunityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Opportunity
|
||||
fields = [
|
||||
'car', 'deal_name', 'deal_value',
|
||||
'car', 'customer', 'stage',
|
||||
]
|
||||
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 19:03
|
||||
|
||||
import django.utils.timezone
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_alter_additionalservices_item'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='is_lead',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='dob',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Date of Birth'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='gender',
|
||||
field=models.CharField(choices=[('m', 'Male'), ('f', 'Female')], default='m', max_length=1, verbose_name='Gender'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='nationality',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Nationality'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='obligations',
|
||||
field=models.PositiveIntegerField(default=1000, verbose_name='Obligations'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='salary',
|
||||
field=models.PositiveIntegerField(default=10000, verbose_name='Salary'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='title',
|
||||
field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company')], default='mr', max_length=10, verbose_name='Title'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 05:46
|
||||
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0006_remove_customer_is_lead_customer_dob_customer_gender_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='nationality',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Country'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='title',
|
||||
field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='deal_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], default='new', max_length=20, verbose_name='Deal Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunitylog',
|
||||
name='new_status',
|
||||
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunitylog',
|
||||
name='old_status',
|
||||
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='Old Status'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,224 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 09:19
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('inventory', '0007_remove_customer_nationality_customer_country_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='notes',
|
||||
options={'verbose_name': 'Note', 'verbose_name_plural': 'Notes'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='notification',
|
||||
options={'ordering': ['-created'], 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='obligations',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='salary',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='opportunity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notification',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_value',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='priority',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='staff',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='staff',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='staff',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='stage',
|
||||
field=models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], default='prospect', max_length=20, verbose_name='Stage'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staff',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staff',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notes',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to='inventory.staff'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Activity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('add_car', 'Add Car'), ('reserve_car', 'Reserve Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to='inventory.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Activity',
|
||||
'verbose_name_plural': 'Activities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Lead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], max_length=20, verbose_name='Title')),
|
||||
('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(db_index=True, max_length=254, unique=True, verbose_name='Email')),
|
||||
('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')),
|
||||
('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')),
|
||||
('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')),
|
||||
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
|
||||
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, max_length=50, verbose_name='Status')),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('assigned', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
|
||||
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead',
|
||||
'verbose_name_plural': 'Leads',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='lead',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeadStatusHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('old_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')),
|
||||
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead Status History',
|
||||
'verbose_name_plural': 'Lead Status Histories',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OpportunityLog',
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0009_alter_staff_managers.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 09:57
|
||||
|
||||
import inventory.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_alter_notes_options_alter_notification_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='staff',
|
||||
managers=[
|
||||
('objects', inventory.models.StaffUserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -23,21 +23,32 @@ from django.db.models import Sum
|
||||
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 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_countries.fields import CountryField
|
||||
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(email=email, password=password, **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'
|
||||
@ -54,6 +65,8 @@ class UnitOfMeasure(models.TextChoices):
|
||||
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)
|
||||
@ -62,6 +75,7 @@ class VatRate(models.Model):
|
||||
def __str__(self):
|
||||
return f"Rate: {self.rate}%"
|
||||
|
||||
|
||||
class CarMake(models.Model, LocalizedNameMixin):
|
||||
id_car_make = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255, blank=True, null=True)
|
||||
@ -382,6 +396,7 @@ class CarFinance(models.Model):
|
||||
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"))
|
||||
@ -545,7 +560,6 @@ class Subscription(models.Model):
|
||||
return self.users.count()
|
||||
|
||||
|
||||
|
||||
class SubscriptionUser(models.Model):
|
||||
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
@ -679,8 +693,10 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
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_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
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")
|
||||
@ -691,33 +707,10 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
return f"{self.name} - {self.get_staff_type_display()}"
|
||||
|
||||
|
||||
class ActionChoices(models.TextChoices):
|
||||
CREATE = "create", _("Create")
|
||||
UPDATE = "update", _("Update")
|
||||
DELETE = "delete", _("Delete")
|
||||
STATUS_CHANGE = "status_change", _("Status Change")
|
||||
|
||||
|
||||
class DealStatus(models.TextChoices):
|
||||
NEW = "new", _("New")
|
||||
PENDING = "pending", _("Pending")
|
||||
CANCELED = "canceled", _("Canceled")
|
||||
COMPLETED = "completed", _("Completed")
|
||||
|
||||
|
||||
class Priority(models.TextChoices):
|
||||
LOW = "low", _("Low")
|
||||
MEDIUM = "medium", _("Medium")
|
||||
HIGH = "high", _("High")
|
||||
|
||||
|
||||
class Sources(models.TextChoices):
|
||||
REFERRALS = "referrals", _("Referrals")
|
||||
WALK_IN = "walk_in", _("Walk In")
|
||||
TOLL_FREE = "toll_free", _("Toll Free")
|
||||
WHATSAPP = "whatsapp", _("WhatsApp")
|
||||
SHOWROOM = "showroom", _("Showroom")
|
||||
WEBSITE = "website", _("Website")
|
||||
TIKTOK = "tiktok", _("TikTok")
|
||||
INSTAGRAM = "instagram", _("Instagram")
|
||||
X = "x", _("X")
|
||||
@ -725,78 +718,125 @@ class Sources(models.TextChoices):
|
||||
MOTORY = "motory", _("Motory")
|
||||
INFLUENCERS = "influencers", _("Influencers")
|
||||
YOUTUBE = "youtube", _("Youtube")
|
||||
EMAIL = "email", _("Email")
|
||||
CAMPAIGN = "campaign", _("Campaign")
|
||||
|
||||
class ContactStatus(models.TextChoices):
|
||||
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")
|
||||
ASSIGNED = "assigned", _("Assigned")
|
||||
IN_PROGRESS = "in_progress", _("In Progress")
|
||||
CONTACTED = "contacted", _("Contacted")
|
||||
ACCEPTED = "accepted", _("Accepted")
|
||||
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 Contact(models.Model):
|
||||
# AGE_RANGES = (
|
||||
# ('18-30', '18 - 30'),
|
||||
# ('31-40', '31 - 40'),
|
||||
# ('41-50', '41 - 50'),
|
||||
# ('51-60', '51 - 60'),
|
||||
# ('61-70', '61 - 70'),
|
||||
# ('71-80', '71 - 80'),
|
||||
# ('81-90', '81 - 90'),
|
||||
# )
|
||||
#
|
||||
# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="contacts")
|
||||
# first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
|
||||
# last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
|
||||
# age = models.CharField(choices=AGE_RANGES, max_length=20, verbose_name=_("Age"))
|
||||
# gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender"))
|
||||
# phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
||||
# email = models.EmailField(verbose_name=_("Email"))
|
||||
# id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, verbose_name=_("Make"))
|
||||
# id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, verbose_name=_("Model"))
|
||||
# year = models.PositiveSmallIntegerField(verbose_name=_("Year"))
|
||||
# status = models.CharField(choices=ContactStatus.choices, max_length=255, verbose_name=_("Status"), default=ContactStatus.NEW)
|
||||
# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
# updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
# enquiry_type = models.CharField(choices=[("quotation", _("Quote")),("testdrive", _("Test drive"))], max_length=50, verbose_name=_("Enquiry Type"))
|
||||
# purchase_method = models.CharField(choices=[("c", _("Cash")),("f", _("Finance"))], max_length=1, verbose_name=_("Purchase Method"))
|
||||
# source = models.CharField(max_length=100, choices=Sources.choices, verbose_name=_("Source"))
|
||||
# salary = models.PositiveIntegerField(verbose_name=_("Salary"))
|
||||
# obligations = models.PositiveIntegerField(verbose_name=_("Obligations"))
|
||||
#
|
||||
# class Meta:
|
||||
# verbose_name = _("Contact")
|
||||
# verbose_name_plural = _("Contacts")
|
||||
#
|
||||
# def __str__(self):
|
||||
# return self.first_name + " " + self.last_name
|
||||
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 Lead(models.Model):
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads")
|
||||
title = models.CharField(max_length=20, choices=Title.choices, verbose_name=_("Title"))
|
||||
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(unique=True, verbose_name=_("Email"), db_index=True)
|
||||
salary = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Salary"))
|
||||
obligations = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Obligations"))
|
||||
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"))
|
||||
assigned = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="assigned", verbose_name=_("Assigned"))
|
||||
priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW,
|
||||
verbose_name=_("Priority"))
|
||||
status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True)
|
||||
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}"
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
dealer = models.ForeignKey(Dealer,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="customers")
|
||||
lead = models.OneToOneField(Lead, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name="converted", verbose_name=_("Lead"))
|
||||
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")
|
||||
)
|
||||
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")
|
||||
)
|
||||
national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID"))
|
||||
country = CountryField(blank=True, verbose_name=_("Country"))
|
||||
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"))
|
||||
is_lead = models.BooleanField(default=False, verbose_name=_("Is Lead"))
|
||||
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Customer")
|
||||
@ -812,65 +852,67 @@ class Customer(models.Model):
|
||||
|
||||
|
||||
class Opportunity(models.Model):
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="opportunities")
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="opportunities")
|
||||
car = models.ForeignKey(Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car"))
|
||||
deal_name = models.CharField(max_length=255, verbose_name=_("Deal Name"))
|
||||
deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value"))
|
||||
deal_status = models.CharField(max_length=20, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status"))
|
||||
priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority"))
|
||||
created_by = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="deals_created")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
|
||||
stage = models.CharField(max_length=20, choices=Stage.choices, verbose_name=_("Stage"))
|
||||
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner"))
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Opportunity")
|
||||
verbose_name_plural = _("Opportunities")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.deal_name} - {self.customer.get_full_name}"
|
||||
return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer.get_full_name}"
|
||||
|
||||
|
||||
class Notes(models.Model):
|
||||
opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="notes")
|
||||
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_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
created_by = models.ForeignKey(Staff, 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 = _("Notes")
|
||||
verbose_name = _("Note")
|
||||
verbose_name_plural = _("Notes")
|
||||
|
||||
def __str__(self):
|
||||
return f"Note by {self.created_by.name} on {self.content_object}"
|
||||
|
||||
class OpportunityLog(models.Model):
|
||||
opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="logs")
|
||||
action = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Action"))
|
||||
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, verbose_name=_("Staff"))
|
||||
old_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("Old Status"))
|
||||
new_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("New Status"))
|
||||
details = models.TextField(blank=True, null=True, verbose_name=_("Details"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
|
||||
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(Staff, 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 = _("Log")
|
||||
verbose_name_plural = _("Logs")
|
||||
ordering = ['-created_at']
|
||||
verbose_name = _("Activity")
|
||||
verbose_name_plural = _("Activities")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_action_display()} by {self.user} on {self.opportunity.deal_name}"
|
||||
return f"{self.get_activity_type_display()} by {self.created_by.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_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Notification")
|
||||
verbose_name_plural = _("Notifications")
|
||||
ordering = ['-created_at']
|
||||
ordering = ['-created']
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
@ -12,7 +12,6 @@ from django_ledger.models import (
|
||||
VendorModel,
|
||||
)
|
||||
from . import models
|
||||
from .models import OpportunityLog
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@ -430,47 +429,47 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Opportunity)
|
||||
def notify_staff_on_deal_status_change(sender, instance, **kwargs):
|
||||
def notify_staff_on_deal_stage_change(sender, instance, **kwargs):
|
||||
if instance.pk:
|
||||
previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||
if previous.deal_status != instance.deal_status:
|
||||
message = f"Deal '{instance.deal_name}' status changed from {previous.deal_status} to {instance.deal_status}."
|
||||
if previous.stage != instance.deal_status:
|
||||
message = f"Deal '{instance.deal_name}' status changed from {previous.stage} to {instance.stage}."
|
||||
models.Notification.objects.create(
|
||||
staff=instance.created_by, message=message
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Opportunity)
|
||||
def log_opportunity_creation(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
models.OpportunityLog.objects.create(
|
||||
opportunity=instance,
|
||||
action="create",
|
||||
user=instance.created_by,
|
||||
details=f"Opportunity '{instance.deal_name}' was created.",
|
||||
)
|
||||
# @receiver(post_save, sender=models.Opportunity)
|
||||
# def log_opportunity_creation(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# models.OpportunityLog.objects.create(
|
||||
# opportunity=instance,
|
||||
# action="create",
|
||||
# user=instance.created_by,
|
||||
# details=f"Opportunity '{instance.deal_name}' was created.",
|
||||
# )
|
||||
|
||||
|
||||
@receiver(pre_save, sender=models.Opportunity)
|
||||
def log_opportunity_update(sender, instance, **kwargs):
|
||||
if instance.pk:
|
||||
previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||
if previous.deal_status != instance.deal_status:
|
||||
models.OpportunityLog.objects.create(
|
||||
opportunity=instance,
|
||||
action="status_change",
|
||||
user=instance.created_by,
|
||||
old_status=previous.deal_status,
|
||||
new_status=instance.deal_status,
|
||||
details=f"Status changed from {previous.deal_status} to {instance.deal_status}.",
|
||||
)
|
||||
else:
|
||||
models.OpportunityLog.objects.create(
|
||||
opportunity=instance,
|
||||
action="update",
|
||||
user=instance.created_by,
|
||||
details=f"Opportunity '{instance.deal_name}' was updated.",
|
||||
)
|
||||
# @receiver(pre_save, sender=models.Opportunity)
|
||||
# def log_opportunity_update(sender, instance, **kwargs):
|
||||
# if instance.pk:
|
||||
# previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||
# if previous.stage != instance.deal_status:
|
||||
# models.OpportunityLog.objects.create(
|
||||
# opportunity=instance,
|
||||
# action="status_change",
|
||||
# user=instance.created_by,
|
||||
# old_status=previous.deal_status,
|
||||
# new_status=instance.deal_status,
|
||||
# details=f"Status changed from {previous.deal_status} to {instance.deal_status}.",
|
||||
# )
|
||||
# else:
|
||||
# models.OpportunityLog.objects.create(
|
||||
# opportunity=instance,
|
||||
# action="update",
|
||||
# user=instance.created_by,
|
||||
# details=f"Opportunity '{instance.deal_name}' was updated.",
|
||||
# )
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.AdditionalServices)
|
||||
|
||||
@ -49,7 +49,7 @@ urlpatterns = [
|
||||
path('crm/opportunities/<int:pk>/edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'),
|
||||
path('crm/opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'),
|
||||
path('crm/opportunities/<int:pk>/delete/', views.delete_opportunity, name='delete_opportunity'),
|
||||
path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
|
||||
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
|
||||
path('crm/notifications/', views.NotificationListView.as_view(), name='notifications_history'),
|
||||
path('crm/fetch_notifications/', views.fetch_notifications, name='fetch_notifications'),
|
||||
path('crm/notifications/<int:pk>/mark_as_read/', views.mark_notification_as_read, name='mark_notification_as_read'),
|
||||
|
||||
@ -758,7 +758,7 @@ class CustomerListView(LoginRequiredMixin, ListView):
|
||||
query = self.request.GET.get("q")
|
||||
dealer = get_user_type(self.request)
|
||||
|
||||
customers = models.Customer.objects.filter(dealer=dealer, is_lead=False)
|
||||
customers = models.Customer.objects.filter(dealer=dealer)
|
||||
|
||||
if query:
|
||||
customers = customers.filter(
|
||||
@ -1275,7 +1275,8 @@ class UserCreateView(
|
||||
success_message = _("User created successfully.")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.dealer = self.request.user.dealer
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.dealer = dealer
|
||||
email = form.cleaned_data["email"]
|
||||
password = "Tenhal@123"
|
||||
user = User.objects.create_user(username=email, email=email, password=password)
|
||||
@ -2390,21 +2391,21 @@ def delete_opportunity(request, pk):
|
||||
return redirect("opportunity_list")
|
||||
|
||||
|
||||
class OpportunityLogsView(LoginRequiredMixin, ListView):
|
||||
model = models.OpportunityLog
|
||||
template_name = "crm/opportunity_logs.html"
|
||||
context_object_name = "logs"
|
||||
|
||||
def get_queryset(self):
|
||||
opportunity_id = self.kwargs["pk"]
|
||||
return models.OpportunityLog.objects.filter(
|
||||
opportunity_id=opportunity_id
|
||||
).order_by("-created_at")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["opportunity"] = models.Opportunity.objects.get(pk=self.kwargs["pk"])
|
||||
return context
|
||||
# class OpportunityLogsView(LoginRequiredMixin, ListView):
|
||||
# model = models.OpportunityLog
|
||||
# template_name = "crm/opportunity_logs.html"
|
||||
# context_object_name = "logs"
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# opportunity_id = self.kwargs["pk"]
|
||||
# return models.OpportunityLog.objects.filter(
|
||||
# opportunity_id=opportunity_id
|
||||
# ).order_by("-created_at")
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# context["opportunity"] = models.Opportunity.objects.get(pk=self.kwargs["pk"])
|
||||
# return context
|
||||
|
||||
|
||||
class NotificationListView(LoginRequiredMixin, ListView):
|
||||
|
||||
@ -36,6 +36,7 @@ django-autoslug==1.9.9
|
||||
django-bootstrap5==24.3
|
||||
django-classy-tags==4.1.0
|
||||
django-cors-headers==4.6.0
|
||||
django-countries==7.6.1
|
||||
django-crispy-forms==2.3
|
||||
django-debug-toolbar==4.4.6
|
||||
django-extensions==3.2.3
|
||||
|
||||
BIN
static/.DS_Store
vendored
BIN
static/flags/.DS_Store
vendored
Normal file
BIN
static/flags/AA.gif
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/flags/AC.gif
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/flags/AE.gif
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/flags/AF.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/AG.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/flags/AJ.gif
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/flags/AL.gif
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
static/flags/AM.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/flags/AN.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/flags/AO.gif
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
static/flags/AQ.gif
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
static/flags/AR.gif
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
static/flags/AS.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/flags/AT.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/flags/AU.gif
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/flags/AV.gif
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
static/flags/AX.gif
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
static/flags/BA.gif
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/flags/BB.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/flags/BC.gif
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/flags/BD.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/BE.gif
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
static/flags/BF.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/flags/BG.gif
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/flags/BH.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/flags/BK.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/BL.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/flags/BM.gif
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
static/flags/BN.gif
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/flags/BO.gif
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/flags/BP.gif
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
static/flags/BQ.gif
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
static/flags/BR.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/flags/BT.gif
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
static/flags/BU.gif
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/flags/BV.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/flags/BX.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/flags/BY.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/flags/CA.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/flags/CB.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/flags/CD.gif
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
static/flags/CE.gif
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
static/flags/CF.gif
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
static/flags/CG.gif
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
static/flags/CH.gif
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/flags/CI.gif
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
static/flags/CJ.gif
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
static/flags/CK.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/flags/CM.gif
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
static/flags/CN.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/flags/CO.gif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/flags/CQ.gif
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
static/flags/CR.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/flags/CS.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/flags/CT.gif
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
static/flags/CU.gif
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
static/flags/CV.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/CW.gif
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
static/flags/CY.gif
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
static/flags/DA.gif
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
static/flags/DJ.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/flags/DO.gif
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/flags/DR.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/DX.gif
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
static/flags/EC.gif
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/flags/EE.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/flags/EG.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/flags/EI.gif
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/flags/EK.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/flags/EN.gif
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/flags/ER.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/flags/ES.gif
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/flags/ET.gif
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/flags/EZ.gif
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
static/flags/FI.gif
Normal file
|
After Width: | Height: | Size: 2.4 KiB |