This commit is contained in:
Marwan Alwali 2025-01-09 13:16:40 +03:00
parent 9ccb432d19
commit 3c9633100a
277 changed files with 829 additions and 181 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
inventory/.DS_Store vendored

Binary file not shown.

View File

@ -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):

View File

@ -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',
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View 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()),
],
),
]

View File

@ -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

View File

@ -12,7 +12,6 @@ from django_ledger.models import (
VendorModel,
)
from . import models
from .models import OpportunityLog
User = get_user_model()
@ -349,47 +348,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)

View File

@ -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'),

View File

@ -752,7 +752,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(
@ -1269,7 +1269,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)
@ -2472,21 +2473,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):

View File

@ -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

Binary file not shown.

BIN
static/flags/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/flags/AA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/flags/AC.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/flags/AE.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
static/flags/AF.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/AG.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/flags/AJ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/flags/AL.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
static/flags/AM.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/flags/AN.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/flags/AO.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
static/flags/AQ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
static/flags/AR.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
static/flags/AS.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/flags/AT.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/flags/AU.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/flags/AV.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
static/flags/AX.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
static/flags/BA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/flags/BB.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/flags/BC.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/flags/BD.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/BE.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
static/flags/BF.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/BG.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
static/flags/BH.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/flags/BK.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/BL.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/BM.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
static/flags/BN.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
static/flags/BO.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/flags/BP.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
static/flags/BQ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
static/flags/BR.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/flags/BT.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
static/flags/BU.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/flags/BV.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/BX.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/flags/BY.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/flags/CA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/flags/CB.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/flags/CD.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
static/flags/CE.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
static/flags/CF.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
static/flags/CG.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
static/flags/CH.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/flags/CI.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
static/flags/CJ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
static/flags/CK.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/flags/CM.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
static/flags/CN.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/flags/CO.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
static/flags/CQ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
static/flags/CR.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/flags/CS.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/flags/CT.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
static/flags/CU.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
static/flags/CV.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/CW.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
static/flags/CY.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
static/flags/DA.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
static/flags/DJ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/DO.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/flags/DR.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/DX.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
static/flags/EC.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/flags/EE.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/flags/EG.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/EI.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
static/flags/EK.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/flags/EN.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/flags/ER.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
static/flags/ES.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/flags/ET.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/flags/EZ.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
static/flags/FI.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Some files were not shown because too many files have changed in this diff Show More