update
BIN
inventory/.DS_Store
vendored
@ -32,7 +32,7 @@ admin.site.register(models.VatRate)
|
|||||||
admin.site.register(models.Customer)
|
admin.site.register(models.Customer)
|
||||||
admin.site.register(models.Opportunity)
|
admin.site.register(models.Opportunity)
|
||||||
admin.site.register(models.Notification)
|
admin.site.register(models.Notification)
|
||||||
admin.site.register(models.OpportunityLog)
|
admin.site.register(models.Lead)
|
||||||
|
|
||||||
@admin.register(models.CarMake)
|
@admin.register(models.CarMake)
|
||||||
class CarMakeAdmin(admin.ModelAdmin):
|
class CarMakeAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from django_countries.widgets import CountrySelectWidget
|
||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
@ -25,7 +26,7 @@ from .models import (
|
|||||||
SaleQuotationCar,
|
SaleQuotationCar,
|
||||||
AdditionalServices,
|
AdditionalServices,
|
||||||
Staff,
|
Staff,
|
||||||
Opportunity, DealStatus, Priority, Sources,
|
Opportunity, Priority, Sources,
|
||||||
)
|
)
|
||||||
from django_ledger.models import ItemModel, InvoiceModel
|
from django_ledger.models import ItemModel, InvoiceModel
|
||||||
from django.forms import ModelMultipleChoiceField, ValidationError
|
from django.forms import ModelMultipleChoiceField, ValidationError
|
||||||
@ -61,21 +62,21 @@ class StaffForm(forms.ModelForm):
|
|||||||
model = Staff
|
model = Staff
|
||||||
fields = ["name", "arabic_name", "phone_number", "staff_type"]
|
fields = ["name", "arabic_name", "phone_number", "staff_type"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
# def __init__(self, *args, **kwargs):
|
||||||
user_instance = kwargs.get("instance")
|
# user_instance = kwargs.get("instance")
|
||||||
if user_instance and user_instance.user:
|
# if user_instance and user_instance.user:
|
||||||
initial = kwargs.setdefault("initial", {})
|
# initial = kwargs.setdefault("initial", {})
|
||||||
initial["email"] = user_instance.user.email
|
# initial["email"] = user_instance.user.email
|
||||||
super().__init__(*args, **kwargs)
|
# super().__init__(*args, **kwargs)
|
||||||
|
#
|
||||||
def save(self, commit=True):
|
# def save(self, commit=True):
|
||||||
staff_instance = super().save(commit=False)
|
# user_instance = super().save(commit=False)
|
||||||
user = staff_instance.user
|
# user = user_instance.user
|
||||||
user.email = self.cleaned_data["email"]
|
# user.email = self.cleaned_data["email"]
|
||||||
if commit:
|
# if commit:
|
||||||
user.save()
|
# user.save()
|
||||||
staff_instance.save()
|
# user_instance.save()
|
||||||
return staff_instance
|
# return user_instance
|
||||||
|
|
||||||
|
|
||||||
# Dealer Form
|
# Dealer Form
|
||||||
@ -105,7 +106,9 @@ class CustomerForm(forms.ModelForm, AddClassMixin):
|
|||||||
"national_id",
|
"national_id",
|
||||||
"phone_number",
|
"phone_number",
|
||||||
"address",
|
"address",
|
||||||
|
"country"
|
||||||
]
|
]
|
||||||
|
widgets = {"country": CountrySelectWidget()}
|
||||||
|
|
||||||
|
|
||||||
class CarForm(
|
class CarForm(
|
||||||
@ -559,6 +562,6 @@ class OpportunityForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Opportunity
|
model = Opportunity
|
||||||
fields = [
|
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 decimal import Decimal, InvalidOperation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.timezone import now
|
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 .utilities.financials import get_financial_value, get_total, get_total_financials
|
||||||
from django.db.models import FloatField
|
from django.db.models import FloatField
|
||||||
from .mixins import LocalizedNameMixin
|
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
|
||||||
|
|
||||||
|
|
||||||
class DealerUserManager(UserManager):
|
class DealerUserManager(UserManager):
|
||||||
def create_user_with_dealer(self, email, password, dealer_name, arabic_name, crn, vrn, address, **extra_fields):
|
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)
|
Dealer.objects.create(user=user, name=dealer_name,arabic_name=arabic_name, crn=crn, vrn=vrn, address=address, **extra_fields)
|
||||||
|
|
||||||
return user
|
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):
|
class UnitOfMeasure(models.TextChoices):
|
||||||
EACH = 'EA', 'Each'
|
EACH = 'EA', 'Each'
|
||||||
PAIR = 'PR', 'Pair'
|
PAIR = 'PR', 'Pair'
|
||||||
@ -54,6 +65,8 @@ class UnitOfMeasure(models.TextChoices):
|
|||||||
SQUARE_METER = 'SQ_M', 'Square Meter'
|
SQUARE_METER = 'SQ_M', 'Square Meter'
|
||||||
PIECE = 'PC', 'Piece'
|
PIECE = 'PC', 'Piece'
|
||||||
BUNDLE = 'BDL', 'Bundle'
|
BUNDLE = 'BDL', 'Bundle'
|
||||||
|
|
||||||
|
|
||||||
class VatRate(models.Model):
|
class VatRate(models.Model):
|
||||||
rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15'))
|
rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15'))
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
@ -62,6 +75,7 @@ class VatRate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Rate: {self.rate}%"
|
return f"Rate: {self.rate}%"
|
||||||
|
|
||||||
|
|
||||||
class CarMake(models.Model, LocalizedNameMixin):
|
class CarMake(models.Model, LocalizedNameMixin):
|
||||||
id_car_make = models.AutoField(primary_key=True)
|
id_car_make = models.AutoField(primary_key=True)
|
||||||
name = models.CharField(max_length=255, blank=True, null=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 = _("Car Financial Details")
|
||||||
verbose_name_plural = _("Car Financial Details")
|
verbose_name_plural = _("Car Financial Details")
|
||||||
|
|
||||||
|
|
||||||
class ExteriorColors(models.Model, LocalizedNameMixin):
|
class ExteriorColors(models.Model, LocalizedNameMixin):
|
||||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||||
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
||||||
@ -545,7 +560,6 @@ class Subscription(models.Model):
|
|||||||
return self.users.count()
|
return self.users.count()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionUser(models.Model):
|
class SubscriptionUser(models.Model):
|
||||||
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(User, 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"))
|
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
||||||
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
||||||
staff_type = models.CharField(choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type"))
|
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"))
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||||
|
|
||||||
|
objects = StaffUserManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Staff")
|
verbose_name = _("Staff")
|
||||||
@ -691,33 +707,10 @@ class Staff(models.Model, LocalizedNameMixin):
|
|||||||
return f"{self.name} - {self.get_staff_type_display()}"
|
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):
|
class Sources(models.TextChoices):
|
||||||
REFERRALS = "referrals", _("Referrals")
|
REFERRALS = "referrals", _("Referrals")
|
||||||
WALK_IN = "walk_in", _("Walk In")
|
|
||||||
TOLL_FREE = "toll_free", _("Toll Free")
|
|
||||||
WHATSAPP = "whatsapp", _("WhatsApp")
|
WHATSAPP = "whatsapp", _("WhatsApp")
|
||||||
SHOWROOM = "showroom", _("Showroom")
|
SHOWROOM = "showroom", _("Showroom")
|
||||||
WEBSITE = "website", _("Website")
|
|
||||||
TIKTOK = "tiktok", _("TikTok")
|
TIKTOK = "tiktok", _("TikTok")
|
||||||
INSTAGRAM = "instagram", _("Instagram")
|
INSTAGRAM = "instagram", _("Instagram")
|
||||||
X = "x", _("X")
|
X = "x", _("X")
|
||||||
@ -725,78 +718,125 @@ class Sources(models.TextChoices):
|
|||||||
MOTORY = "motory", _("Motory")
|
MOTORY = "motory", _("Motory")
|
||||||
INFLUENCERS = "influencers", _("Influencers")
|
INFLUENCERS = "influencers", _("Influencers")
|
||||||
YOUTUBE = "youtube", _("Youtube")
|
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")
|
NEW = "new", _("New")
|
||||||
PENDING = "pending", _("Pending")
|
PENDING = "pending", _("Pending")
|
||||||
ASSIGNED = "assigned", _("Assigned")
|
ASSIGNED = "assigned", _("Assigned")
|
||||||
|
IN_PROGRESS = "in_progress", _("In Progress")
|
||||||
CONTACTED = "contacted", _("Contacted")
|
CONTACTED = "contacted", _("Contacted")
|
||||||
ACCEPTED = "accepted", _("Accepted")
|
|
||||||
QUALIFIED = "qualified", _("Qualified")
|
QUALIFIED = "qualified", _("Qualified")
|
||||||
CANCELED = "canceled", _("Canceled")
|
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):
|
class ActionChoices(models.TextChoices):
|
||||||
# AGE_RANGES = (
|
CALL = "call", _("Call")
|
||||||
# ('18-30', '18 - 30'),
|
SMS = "sms", _("SMS")
|
||||||
# ('31-40', '31 - 40'),
|
EMAIL = "email", _("Email")
|
||||||
# ('41-50', '41 - 50'),
|
WHATSAPP = "whatsapp", _("WhatsApp")
|
||||||
# ('51-60', '51 - 60'),
|
VISIT = "visit", _("Visit")
|
||||||
# ('61-70', '61 - 70'),
|
ADD_CAR = "add_car", _("Add Car")
|
||||||
# ('71-80', '71 - 80'),
|
RESERVE_CAR = "reserve_car", _("Reserve Car")
|
||||||
# ('81-90', '81 - 90'),
|
REMOVE_CAR = "remove_car", _("Remove Car")
|
||||||
# )
|
CREATE_QUOTATION = "create_quotation", _("Create Quotation")
|
||||||
#
|
CANCEL_QUOTATION = "cancel_quotation", _("Cancel Quotation")
|
||||||
# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="contacts")
|
CREATE_ORDER = "create_order", _("Create Order")
|
||||||
# first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
|
CANCEL_ORDER = "cancel_order", _("Cancel Order")
|
||||||
# last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
|
CREATE_INVOICE = "create_invoice", _("Create Invoice")
|
||||||
# age = models.CharField(choices=AGE_RANGES, max_length=20, verbose_name=_("Age"))
|
CANCEL_INVOICE = "cancel_invoice", _("Cancel Invoice")
|
||||||
# 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 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):
|
class Customer(models.Model):
|
||||||
dealer = models.ForeignKey(Dealer,
|
lead = models.OneToOneField(Lead, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
on_delete=models.CASCADE,
|
related_name="converted", verbose_name=_("Lead"))
|
||||||
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"))
|
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
|
||||||
middle_name = models.CharField(
|
middle_name = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Middle Name"))
|
||||||
max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
|
|
||||||
)
|
|
||||||
last_name = models.CharField(max_length=50, verbose_name=_("Last 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"))
|
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
||||||
national_id = models.CharField(
|
national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID"))
|
||||||
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"))
|
||||||
phone_number = PhoneNumberField(
|
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
|
||||||
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"))
|
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:
|
class Meta:
|
||||||
verbose_name = _("Customer")
|
verbose_name = _("Customer")
|
||||||
@ -812,65 +852,67 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Opportunity(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")
|
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"))
|
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"))
|
stage = models.CharField(max_length=20, choices=Stage.choices, verbose_name=_("Stage"))
|
||||||
deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value"))
|
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner"))
|
||||||
deal_status = models.CharField(max_length=20, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status"))
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||||
priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority"))
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||||
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"))
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Opportunity")
|
verbose_name = _("Opportunity")
|
||||||
verbose_name_plural = _("Opportunities")
|
verbose_name_plural = _("Opportunities")
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
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"))
|
note = models.TextField(verbose_name=_("Note"))
|
||||||
created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="notes_created")
|
created_by = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes_created")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Notes")
|
verbose_name = _("Note")
|
||||||
verbose_name_plural = _("Notes")
|
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")
|
class Activity(models.Model):
|
||||||
action = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Action"))
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, verbose_name=_("Staff"))
|
object_id = models.PositiveIntegerField()
|
||||||
old_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("Old Status"))
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
new_status = models.CharField(max_length=50, choices=DealStatus.choices, null=True, blank=True, verbose_name=_("New Status"))
|
activity_type = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Activity Type"))
|
||||||
details = models.TextField(blank=True, null=True, verbose_name=_("Details"))
|
notes = models.TextField(blank=True, null=True, verbose_name=_("Notes"))
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
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:
|
class Meta:
|
||||||
verbose_name = _("Log")
|
verbose_name = _("Activity")
|
||||||
verbose_name_plural = _("Logs")
|
verbose_name_plural = _("Activities")
|
||||||
ordering = ['-created_at']
|
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class Notification(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
|
||||||
message = models.CharField(max_length=255, verbose_name=_("Message"))
|
message = models.CharField(max_length=255, verbose_name=_("Message"))
|
||||||
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
|
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:
|
class Meta:
|
||||||
verbose_name = _("Notification")
|
verbose_name = _("Notification")
|
||||||
verbose_name_plural = _("Notifications")
|
verbose_name_plural = _("Notifications")
|
||||||
ordering = ['-created_at']
|
ordering = ['-created']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.message
|
return self.message
|
||||||
|
|||||||
@ -12,7 +12,6 @@ from django_ledger.models import (
|
|||||||
VendorModel,
|
VendorModel,
|
||||||
)
|
)
|
||||||
from . import models
|
from . import models
|
||||||
from .models import OpportunityLog
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@ -349,47 +348,47 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Opportunity)
|
@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:
|
if instance.pk:
|
||||||
previous = models.Opportunity.objects.get(pk=instance.pk)
|
previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||||
if previous.deal_status != instance.deal_status:
|
if previous.stage != instance.deal_status:
|
||||||
message = f"Deal '{instance.deal_name}' status changed from {previous.deal_status} to {instance.deal_status}."
|
message = f"Deal '{instance.deal_name}' status changed from {previous.stage} to {instance.stage}."
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
staff=instance.created_by, message=message
|
staff=instance.created_by, message=message
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Opportunity)
|
# @receiver(post_save, sender=models.Opportunity)
|
||||||
def log_opportunity_creation(sender, instance, created, **kwargs):
|
# def log_opportunity_creation(sender, instance, created, **kwargs):
|
||||||
if created:
|
# if created:
|
||||||
models.OpportunityLog.objects.create(
|
# models.OpportunityLog.objects.create(
|
||||||
opportunity=instance,
|
# opportunity=instance,
|
||||||
action="create",
|
# action="create",
|
||||||
user=instance.created_by,
|
# user=instance.created_by,
|
||||||
details=f"Opportunity '{instance.deal_name}' was created.",
|
# details=f"Opportunity '{instance.deal_name}' was created.",
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=models.Opportunity)
|
# @receiver(pre_save, sender=models.Opportunity)
|
||||||
def log_opportunity_update(sender, instance, **kwargs):
|
# def log_opportunity_update(sender, instance, **kwargs):
|
||||||
if instance.pk:
|
# if instance.pk:
|
||||||
previous = models.Opportunity.objects.get(pk=instance.pk)
|
# previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||||
if previous.deal_status != instance.deal_status:
|
# if previous.stage != instance.deal_status:
|
||||||
models.OpportunityLog.objects.create(
|
# models.OpportunityLog.objects.create(
|
||||||
opportunity=instance,
|
# opportunity=instance,
|
||||||
action="status_change",
|
# action="status_change",
|
||||||
user=instance.created_by,
|
# user=instance.created_by,
|
||||||
old_status=previous.deal_status,
|
# old_status=previous.deal_status,
|
||||||
new_status=instance.deal_status,
|
# new_status=instance.deal_status,
|
||||||
details=f"Status changed from {previous.deal_status} to {instance.deal_status}.",
|
# details=f"Status changed from {previous.deal_status} to {instance.deal_status}.",
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
models.OpportunityLog.objects.create(
|
# models.OpportunityLog.objects.create(
|
||||||
opportunity=instance,
|
# opportunity=instance,
|
||||||
action="update",
|
# action="update",
|
||||||
user=instance.created_by,
|
# user=instance.created_by,
|
||||||
details=f"Opportunity '{instance.deal_name}' was updated.",
|
# details=f"Opportunity '{instance.deal_name}' was updated.",
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.AdditionalServices)
|
@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/<int:pk>/edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'),
|
||||||
path('crm/opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'),
|
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>/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/notifications/', views.NotificationListView.as_view(), name='notifications_history'),
|
||||||
path('crm/fetch_notifications/', views.fetch_notifications, name='fetch_notifications'),
|
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'),
|
path('crm/notifications/<int:pk>/mark_as_read/', views.mark_notification_as_read, name='mark_notification_as_read'),
|
||||||
|
|||||||
@ -752,7 +752,7 @@ class CustomerListView(LoginRequiredMixin, ListView):
|
|||||||
query = self.request.GET.get("q")
|
query = self.request.GET.get("q")
|
||||||
dealer = get_user_type(self.request)
|
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:
|
if query:
|
||||||
customers = customers.filter(
|
customers = customers.filter(
|
||||||
@ -1269,7 +1269,8 @@ class UserCreateView(
|
|||||||
success_message = _("User created successfully.")
|
success_message = _("User created successfully.")
|
||||||
|
|
||||||
def form_valid(self, form):
|
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"]
|
email = form.cleaned_data["email"]
|
||||||
password = "Tenhal@123"
|
password = "Tenhal@123"
|
||||||
user = User.objects.create_user(username=email, email=email, password=password)
|
user = User.objects.create_user(username=email, email=email, password=password)
|
||||||
@ -2472,21 +2473,21 @@ def delete_opportunity(request, pk):
|
|||||||
return redirect("opportunity_list")
|
return redirect("opportunity_list")
|
||||||
|
|
||||||
|
|
||||||
class OpportunityLogsView(LoginRequiredMixin, ListView):
|
# class OpportunityLogsView(LoginRequiredMixin, ListView):
|
||||||
model = models.OpportunityLog
|
# model = models.OpportunityLog
|
||||||
template_name = "crm/opportunity_logs.html"
|
# template_name = "crm/opportunity_logs.html"
|
||||||
context_object_name = "logs"
|
# context_object_name = "logs"
|
||||||
|
#
|
||||||
def get_queryset(self):
|
# def get_queryset(self):
|
||||||
opportunity_id = self.kwargs["pk"]
|
# opportunity_id = self.kwargs["pk"]
|
||||||
return models.OpportunityLog.objects.filter(
|
# return models.OpportunityLog.objects.filter(
|
||||||
opportunity_id=opportunity_id
|
# opportunity_id=opportunity_id
|
||||||
).order_by("-created_at")
|
# ).order_by("-created_at")
|
||||||
|
#
|
||||||
def get_context_data(self, **kwargs):
|
# def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
# context = super().get_context_data(**kwargs)
|
||||||
context["opportunity"] = models.Opportunity.objects.get(pk=self.kwargs["pk"])
|
# context["opportunity"] = models.Opportunity.objects.get(pk=self.kwargs["pk"])
|
||||||
return context
|
# return context
|
||||||
|
|
||||||
|
|
||||||
class NotificationListView(LoginRequiredMixin, ListView):
|
class NotificationListView(LoginRequiredMixin, ListView):
|
||||||
|
|||||||
@ -36,6 +36,7 @@ django-autoslug==1.9.9
|
|||||||
django-bootstrap5==24.3
|
django-bootstrap5==24.3
|
||||||
django-classy-tags==4.1.0
|
django-classy-tags==4.1.0
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.6.0
|
||||||
|
django-countries==7.6.1
|
||||||
django-crispy-forms==2.3
|
django-crispy-forms==2.3
|
||||||
django-debug-toolbar==4.4.6
|
django-debug-toolbar==4.4.6
|
||||||
django-extensions==3.2.3
|
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 |