update in opportunity
This commit is contained in:
parent
93674f646b
commit
040e26b3f9
@ -8,11 +8,15 @@ from django.core.validators import RegexValidator
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
|
||||
from .models import Status, Stage
|
||||
from .mixins import AddClassMixin
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django_ledger.forms.invoice import InvoiceModelCreateForm as InvoiceModelCreateFormBase
|
||||
from django_ledger.forms.estimate import EstimateModelCreateForm as EstimateModelCreateFormBase
|
||||
from django_ledger.forms.invoice import (
|
||||
InvoiceModelCreateForm as InvoiceModelCreateFormBase,
|
||||
)
|
||||
from django_ledger.forms.estimate import (
|
||||
EstimateModelCreateForm as EstimateModelCreateFormBase,
|
||||
)
|
||||
|
||||
from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase
|
||||
from django_ledger.forms.vendor import VendorModelForm
|
||||
@ -38,10 +42,23 @@ from .models import (
|
||||
# SaleQuotationCar,
|
||||
AdditionalServices,
|
||||
Staff,
|
||||
Opportunity, Priority, Sources, Lead, Activity, Notes, CarModel,SaleOrder,CarMake
|
||||
Opportunity,
|
||||
Priority,
|
||||
Sources,
|
||||
Lead,
|
||||
Activity,
|
||||
Notes,
|
||||
CarModel,
|
||||
SaleOrder,
|
||||
CarMake,
|
||||
)
|
||||
from django_ledger import models as ledger_models
|
||||
from django.forms import ModelMultipleChoiceField, ValidationError, DateInput,DateTimeInput
|
||||
from django.forms import (
|
||||
ModelMultipleChoiceField,
|
||||
ValidationError,
|
||||
DateInput,
|
||||
DateTimeInput,
|
||||
)
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django.forms import formset_factory
|
||||
@ -111,27 +128,22 @@ class CustomerForm(forms.Form):
|
||||
last_name = forms.CharField()
|
||||
national_id = forms.CharField(max_length=10)
|
||||
email = forms.EmailField()
|
||||
phone_number = PhoneNumberField(
|
||||
min_length=10,
|
||||
max_length=10,
|
||||
region="SA",
|
||||
)
|
||||
phone_number = PhoneNumberField(region="SA")
|
||||
address = forms.CharField()
|
||||
|
||||
|
||||
|
||||
class OrganizationForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
arabic_name = forms.CharField()
|
||||
email = forms.EmailField()
|
||||
phone_number = PhoneNumberField(
|
||||
min_length=10,
|
||||
max_length=10,
|
||||
region="SA",
|
||||
)
|
||||
phone_number = PhoneNumberField(region="SA")
|
||||
crn = forms.CharField()
|
||||
vrn = forms.CharField()
|
||||
address = forms.CharField()
|
||||
contact_person = forms.CharField(required=False)
|
||||
logo = forms.ImageField(required=False)
|
||||
|
||||
|
||||
# class CustomerForm(forms.ModelForm, AddClassMixin):
|
||||
# class Meta:
|
||||
# model = Customer
|
||||
@ -193,7 +205,9 @@ class CarForm(
|
||||
(obj.id_car_model, obj.get_local_name()) for obj in queryset
|
||||
]
|
||||
if "vendor" in self.fields:
|
||||
self.fields["vendor"].queryset = ledger_models.VendorModel.objects.filter(active=True)
|
||||
self.fields["vendor"].queryset = ledger_models.VendorModel.objects.filter(
|
||||
active=True
|
||||
)
|
||||
# queryset = self.fields["vendor"].queryset
|
||||
# self.fields["vendor"].choices = [
|
||||
# (obj.pk, obj.get_local_name()) for obj in queryset
|
||||
@ -303,11 +317,10 @@ class CarRegistrationForm(forms.ModelForm):
|
||||
fields = ["plate_number", "text1", "text2", "text3", "registration_date"]
|
||||
|
||||
widgets = {
|
||||
'registration_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
||||
"registration_date": forms.DateTimeInput(attrs={"type": "datetime-local"}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
# class VendorForm(VendorModelForm):
|
||||
# pass
|
||||
class VendorForm(forms.ModelForm):
|
||||
@ -517,8 +530,6 @@ class WizardForm2(forms.Form):
|
||||
|
||||
phone_number = PhoneNumberField(
|
||||
label=_("Phone Number"),
|
||||
min_length=10,
|
||||
max_length=10,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control form-control-sm",
|
||||
@ -598,7 +609,9 @@ class WizardForm3(forms.Form):
|
||||
|
||||
class ItemForm(forms.Form):
|
||||
item = forms.ModelChoiceField(
|
||||
queryset=ledger_models.ItemModel.objects.all(), label="Item", required=True,
|
||||
queryset=ledger_models.ItemModel.objects.all(),
|
||||
label="Item",
|
||||
required=True,
|
||||
validators=[MinLengthValidator(5)],
|
||||
)
|
||||
quantity = forms.DecimalField(label="Quantity", required=True)
|
||||
@ -609,7 +622,9 @@ class ItemForm(forms.Form):
|
||||
|
||||
class PaymentForm(forms.Form):
|
||||
invoice = forms.ModelChoiceField(
|
||||
queryset=ledger_models.InvoiceModel.objects.all(), label="Invoice", required=False
|
||||
queryset=ledger_models.InvoiceModel.objects.all(),
|
||||
label="Invoice",
|
||||
required=False,
|
||||
)
|
||||
bill = forms.ModelChoiceField(
|
||||
queryset=ledger_models.BillModel.objects.all(), label="Bill", required=False
|
||||
@ -626,13 +641,15 @@ class PaymentForm(forms.Form):
|
||||
label="Payment Method",
|
||||
required=True,
|
||||
)
|
||||
payment_date = forms.DateField(label="Payment Date", widget=DateInput(attrs={'type': 'date'}), required=True)
|
||||
payment_date = forms.DateField(
|
||||
label="Payment Date", widget=DateInput(attrs={"type": "date"}), required=True
|
||||
)
|
||||
|
||||
def clean_amount(self):
|
||||
invoice = self.cleaned_data['invoice']
|
||||
bill = self.cleaned_data['bill']
|
||||
invoice = self.cleaned_data["invoice"]
|
||||
bill = self.cleaned_data["bill"]
|
||||
model = invoice if invoice else bill
|
||||
amount = self.cleaned_data['amount']
|
||||
amount = self.cleaned_data["amount"]
|
||||
if amount + model.amount_paid > model.amount_due:
|
||||
raise forms.ValidationError("Payment amount is greater than amount due")
|
||||
if amount <= 0:
|
||||
@ -652,103 +669,136 @@ class EmailForm(forms.Form):
|
||||
|
||||
|
||||
class LeadForm(forms.ModelForm):
|
||||
id_car_make = forms.ModelChoiceField(label="Make",
|
||||
id_car_make = forms.ModelChoiceField(
|
||||
label="Make",
|
||||
queryset=CarMake.objects.filter(is_sa_import=True),
|
||||
widget=forms.Select(attrs={"class": "form-control form-control-sm","hx-get":"","hx-include":"#id_id_car_make","hx-select":"#div_id_id_car_model","hx-target":"#div_id_id_car_model","hx-swap":"outerHTML"}),
|
||||
required=True
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
"class": "form-control form-control-sm",
|
||||
"hx-get": "",
|
||||
"hx-include": "#id_id_car_make",
|
||||
"hx-select": "#div_id_id_car_model",
|
||||
"hx-target": "#div_id_id_car_model",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-on::before-request": "document.querySelector('#id_id_car_model').setAttribute('disabled', true)",
|
||||
"hx-on::after-request": "document.querySelector('#id_id_car_model').removeAttribute('disabled')",
|
||||
}
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
id_car_model = forms.ModelChoiceField(label="Model", queryset=CarModel.objects.none(),widget=forms.Select(attrs={"class": "form-control form-control-sm"}),required=True)
|
||||
id_car_model = forms.ModelChoiceField(
|
||||
label="Model",
|
||||
queryset=CarModel.objects.none(),
|
||||
widget=forms.Select(attrs={"class": "form-control form-control-sm"}),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Lead
|
||||
fields = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone_number',
|
||||
'address',
|
||||
'id_car_make',
|
||||
'id_car_model',
|
||||
'year',
|
||||
'source',
|
||||
'channel',
|
||||
'staff',
|
||||
'priority',
|
||||
]
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"phone_number",
|
||||
"address",
|
||||
"id_car_make",
|
||||
"id_car_model",
|
||||
"year",
|
||||
"source",
|
||||
"channel",
|
||||
"staff",
|
||||
"priority",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "id_car_make" in self.fields:
|
||||
queryset = self.fields["id_car_make"].queryset.filter(is_sa_import=True)
|
||||
self.fields["id_car_make"].choices = [
|
||||
(obj.id_car_make, obj.get_local_name()) for obj in queryset
|
||||
]
|
||||
|
||||
|
||||
class ScheduleForm(forms.ModelForm):
|
||||
scheduled_at = forms.DateTimeField(widget=DateTimeInput(attrs={'type': 'datetime-local'}))
|
||||
scheduled_at = forms.DateTimeField(
|
||||
widget=DateTimeInput(attrs={"type": "datetime-local"})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Schedule
|
||||
fields = ['purpose','scheduled_type', 'scheduled_at','duration', 'notes']
|
||||
fields = ["purpose", "scheduled_type", "scheduled_at", "duration", "notes"]
|
||||
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Notes
|
||||
fields = ['note']
|
||||
fields = ["note"]
|
||||
|
||||
|
||||
class ActivityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = ['activity_type', 'notes']
|
||||
fields = ["activity_type", "notes"]
|
||||
|
||||
|
||||
class OpportunityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Opportunity
|
||||
fields = ['customer', 'car', 'stage', 'probability', 'staff', 'closing_date']
|
||||
fields = ["customer", "car", "stage", "probability", "staff", "closing_date"]
|
||||
|
||||
|
||||
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['cash_account'].widget = forms.HiddenInput()
|
||||
self.fields['prepaid_account'].widget = forms.HiddenInput()
|
||||
self.fields['unearned_account'].widget = forms.HiddenInput()
|
||||
self.fields['date_draft'] = forms.DateField(widget=DateInput(attrs={'type': 'date'}))
|
||||
self.fields["cash_account"].widget = forms.HiddenInput()
|
||||
self.fields["prepaid_account"].widget = forms.HiddenInput()
|
||||
self.fields["unearned_account"].widget = forms.HiddenInput()
|
||||
self.fields["date_draft"] = forms.DateField(
|
||||
widget=DateInput(attrs={"type": "date"})
|
||||
)
|
||||
|
||||
def get_customer_queryset(self):
|
||||
if 'customer' in self.fields:
|
||||
self.fields['customer'].queryset = self.USER_MODEL.dealer.entity.get_customers()
|
||||
if "customer" in self.fields:
|
||||
self.fields[
|
||||
"customer"
|
||||
].queryset = self.USER_MODEL.dealer.entity.get_customers()
|
||||
|
||||
|
||||
class BillModelCreateForm(BillModelCreateFormBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['cash_account'].widget = forms.HiddenInput()
|
||||
self.fields['prepaid_account'].widget = forms.HiddenInput()
|
||||
self.fields['unearned_account'].widget = forms.HiddenInput()
|
||||
self.fields['date_draft'] = forms.DateField(widget=DateInput(attrs={'type': 'date'}))
|
||||
self.fields["cash_account"].widget = forms.HiddenInput()
|
||||
self.fields["prepaid_account"].widget = forms.HiddenInput()
|
||||
self.fields["unearned_account"].widget = forms.HiddenInput()
|
||||
self.fields["date_draft"] = forms.DateField(
|
||||
widget=DateInput(attrs={"type": "date"})
|
||||
)
|
||||
|
||||
|
||||
class SaleOrderForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SaleOrder
|
||||
fields = ['estimate','payment_method', 'comments']
|
||||
widgets = {
|
||||
'comments': forms.Textarea(attrs={'rows': 3}),
|
||||
fields = ["estimate", "payment_method", "comments"]
|
||||
widgets = {
|
||||
"comments": forms.Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
|
||||
class EstimateModelCreateForm(EstimateModelCreateFormBase):
|
||||
class Meta:
|
||||
model = ledger_models.EstimateModel
|
||||
fields = ['title', 'customer', 'terms']
|
||||
fields = ["title","customer", "terms"]
|
||||
widgets = {
|
||||
'customer': forms.Select(attrs={
|
||||
'id': 'djl-customer-estimate-customer-input',
|
||||
'class': 'input',
|
||||
'label': _('Customer'),
|
||||
}),
|
||||
"customer": forms.Select(
|
||||
attrs={
|
||||
"id": "djl-customer-estimate-customer-input",
|
||||
"class": "input",
|
||||
"label": _("Customer"),
|
||||
}
|
||||
),
|
||||
'terms': forms.Select(attrs={
|
||||
'id': 'djl-customer-estimate-terms-input',
|
||||
'class': 'input',
|
||||
@ -757,23 +807,57 @@ class EstimateModelCreateForm(EstimateModelCreateFormBase):
|
||||
'title': forms.TextInput(attrs={
|
||||
'id': 'djl-customer-job-title-input',
|
||||
'class': 'input' + ' is-large',
|
||||
|
||||
'label': _('Title'),
|
||||
})
|
||||
}
|
||||
labels = {
|
||||
'title': _('Title'),
|
||||
'terms': _('Terms'),
|
||||
'customer': _('Customer'),
|
||||
"customer": _("Customer"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, entity_slug, user_model, **kwargs):
|
||||
super(EstimateModelCreateForm, self).__init__(*args, entity_slug=entity_slug, user_model=user_model, **kwargs)
|
||||
super(EstimateModelCreateForm, self).__init__(
|
||||
*args, entity_slug=entity_slug, user_model=user_model, **kwargs
|
||||
)
|
||||
self.ENTITY_SLUG = entity_slug
|
||||
self.USER_MODEL = user_model
|
||||
self.fields['customer'].queryset = self.get_customer_queryset()
|
||||
self.fields["customer"].queryset = self.get_customer_queryset()
|
||||
|
||||
def get_customer_queryset(self):
|
||||
return self.USER_MODEL.dealer.entity.get_customers()
|
||||
|
||||
|
||||
|
||||
class OpportunityStatusForm(forms.Form):
|
||||
status = forms.ChoiceField(
|
||||
label="Status",
|
||||
choices=Status.choices,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
"class": "form-control form-control-sm",
|
||||
"hx-get": "{% url 'opportunity_update_status' opportunity.id %}",
|
||||
"hx-target": ".other-information",
|
||||
"hx-select": ".other-information",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-on::after-request": "this.setAttribute('disabled','true')",
|
||||
"disabled": "disabled",
|
||||
}
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
stage = forms.ChoiceField(
|
||||
label="Stage",
|
||||
choices=Stage.choices,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
"class": "form-control form-control-sm",
|
||||
"hx-target": ".other-information",
|
||||
"hx-select": ".other-information",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-on::after-request": "this.setAttribute('disabled','true')",
|
||||
"disabled": "disabled",
|
||||
}
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
19
inventory/migrations/0019_opportunity_lead.py
Normal file
19
inventory/migrations/0019_opportunity_lead.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-12 10:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0018_customer_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='lead',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.lead'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0020_alter_opportunity_closing_date.py
Normal file
18
inventory/migrations/0020_alter_opportunity_closing_date.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-12 10:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0019_opportunity_lead'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='closing_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Closing Date'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0021_alter_opportunity_lead.py
Normal file
19
inventory/migrations/0021_alter_opportunity_lead.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-12 10:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0020_alter_opportunity_closing_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='lead',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead'),
|
||||
),
|
||||
]
|
||||
21
inventory/migrations/0022_opportunity_estimate.py
Normal file
21
inventory/migrations/0022_opportunity_estimate.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-12 14:09
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.DJANGO_LEDGER_ESTIMATE_MODEL),
|
||||
('inventory', '0021_alter_opportunity_lead'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='estimate',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to=settings.DJANGO_LEDGER_ESTIMATE_MODEL),
|
||||
),
|
||||
]
|
||||
@ -1203,8 +1203,10 @@ class Lead(models.Model):
|
||||
customer.additional_info = {}
|
||||
customer.additional_info.update({"info":self.to_dict()})
|
||||
self.customer = customer
|
||||
self.status = Status.QUALIFIED
|
||||
customer.save()
|
||||
self.save()
|
||||
return customer
|
||||
|
||||
def get_latest_schedule(self):
|
||||
return self.schedules.order_by('-scheduled_at').first()
|
||||
@ -1307,18 +1309,19 @@ class Opportunity(models.Model):
|
||||
related_name="owner",
|
||||
verbose_name=_("Owner"),
|
||||
)
|
||||
lead = models.OneToOneField("Lead",related_name="opportunity", on_delete=models.CASCADE,null=True,blank=True)
|
||||
probability = models.PositiveIntegerField(validators=[validate_probability])
|
||||
closing_date = models.DateField(verbose_name=_("Closing Date"))
|
||||
closing_date = models.DateField(verbose_name=_("Closing Date"),null=True,blank=True)
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
|
||||
closed = models.BooleanField(default=False, verbose_name=_("Closed"))
|
||||
|
||||
estimate = models.OneToOneField(EstimateModel, related_name="opportunity",on_delete=models.SET_NULL,null=True,blank=True)
|
||||
class Meta:
|
||||
verbose_name = _("Opportunity")
|
||||
verbose_name_plural = _("Opportunities")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer}"
|
||||
return f"Opportunity for {self.customer.customer_name}"
|
||||
|
||||
|
||||
class Notes(models.Model):
|
||||
|
||||
@ -664,21 +664,35 @@ def create_customer(sender, instance, created, **kwargs):
|
||||
print(f"Customer created: {name}")
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Customer)
|
||||
@receiver(post_save, sender=models.CustomerModel)
|
||||
def create_customer_user(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
user = User.objects.create(
|
||||
username=instance.email,
|
||||
email=instance.email,
|
||||
password=None,
|
||||
first_name=instance.first_name,
|
||||
last_name=instance.last_name
|
||||
email=instance.email,
|
||||
first_name=instance.additional_info.get('first_name',''),
|
||||
last_name=instance.additional_info.get('last_name','')
|
||||
)
|
||||
user.is_active = True
|
||||
user.is_staff = True
|
||||
user.is_active = False
|
||||
user.is_staff = False
|
||||
user.save()
|
||||
instance.user = user
|
||||
instance.save()
|
||||
# @receiver(post_save, sender=models.Customer)
|
||||
# def create_customer_user(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# user = User.objects.create(
|
||||
# username=instance.email,
|
||||
# email=instance.email,
|
||||
# password=None,
|
||||
# first_name=instance.first_name,
|
||||
# last_name=instance.last_name
|
||||
# )
|
||||
# user.is_active = True
|
||||
# user.is_staff = True
|
||||
# user.save()
|
||||
# instance.user = user
|
||||
# instance.save()
|
||||
|
||||
|
||||
# Create Item
|
||||
|
||||
@ -143,6 +143,11 @@ urlpatterns = [
|
||||
views.delete_opportunity,
|
||||
name="delete_opportunity",
|
||||
),
|
||||
path(
|
||||
"crm/opportunities/<int:pk>/opportunity_update_status/",
|
||||
views.opportunity_update_status,
|
||||
name="opportunity_update_status",
|
||||
),
|
||||
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
|
||||
path(
|
||||
"crm/notifications/",
|
||||
@ -414,6 +419,7 @@ urlpatterns = [
|
||||
name="estimate_detail",
|
||||
),
|
||||
path("sales/estimates/create/", views.create_estimate, name="estimate_create"),
|
||||
path("sales/estimates/create/<int:pk>/", views.create_estimate, name="estimate_create_from_opportunity"),
|
||||
path(
|
||||
"sales/estimates/<uuid:pk>/estimate_mark_as/",
|
||||
views.estimate_mark_as,
|
||||
|
||||
@ -1256,35 +1256,39 @@ def add_activity_to_customer(request, pk):
|
||||
|
||||
|
||||
def CustomerCreateView(request):
|
||||
if request.method == "POST":
|
||||
customer_dict = {
|
||||
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
|
||||
}
|
||||
dealer = get_user_type(request)
|
||||
customer_name = (
|
||||
customer_dict["first_name"]
|
||||
+ " "
|
||||
+ customer_dict["middle_name"]
|
||||
+ " "
|
||||
+ customer_dict["last_name"]
|
||||
)
|
||||
|
||||
instance = dealer.entity.create_customer(
|
||||
customer_model_kwargs={
|
||||
"customer_name": customer_name,
|
||||
"address_1": customer_dict["address"],
|
||||
"phone": customer_dict["phone_number"],
|
||||
"email": customer_dict["email"],
|
||||
}
|
||||
)
|
||||
customer_dict["pk"] = str(instance.pk)
|
||||
instance.additional_info["customer_info"] = customer_dict
|
||||
instance.additional_info["type"] = "customer"
|
||||
instance.save()
|
||||
messages.success(request, _("Customer created successfully."))
|
||||
return redirect("customer_list")
|
||||
|
||||
form = forms.CustomerForm()
|
||||
if request.method == "POST":
|
||||
dealer = get_user_type(request)
|
||||
if dealer.entity.get_customers().filter(email=request.POST["email"]).exists():
|
||||
messages.error(request, _("Customer with this email already exists."))
|
||||
form = forms.CustomerForm(request.POST)
|
||||
else:
|
||||
customer_dict = {
|
||||
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
|
||||
}
|
||||
customer_name = (
|
||||
customer_dict["first_name"]
|
||||
+ " "
|
||||
+ customer_dict["middle_name"]
|
||||
+ " "
|
||||
+ customer_dict["last_name"]
|
||||
)
|
||||
instance = dealer.entity.create_customer(
|
||||
customer_model_kwargs={
|
||||
"customer_name": customer_name,
|
||||
"address_1": customer_dict["address"],
|
||||
"phone": customer_dict["phone_number"],
|
||||
"email": customer_dict["email"],
|
||||
}
|
||||
)
|
||||
customer_dict["pk"] = str(instance.pk)
|
||||
instance.additional_info = {}
|
||||
instance.additional_info["customer_info"] = customer_dict
|
||||
instance.additional_info["type"] = "customer"
|
||||
instance.save()
|
||||
messages.success(request, _("Customer created successfully."))
|
||||
return redirect("customer_list")
|
||||
|
||||
return render(request, "customers/customer_form.html", {"form": form})
|
||||
|
||||
|
||||
@ -2405,11 +2409,11 @@ class EstimateListView(LoginRequiredMixin, ListView):
|
||||
|
||||
# @csrf_exempt
|
||||
@login_required
|
||||
def create_estimate(request):
|
||||
def create_estimate(request,pk=None):
|
||||
dealer = get_user_type(request)
|
||||
entity = dealer.entity
|
||||
|
||||
if request.method == "POST":
|
||||
if request.method == "POST":
|
||||
# try:
|
||||
data = json.loads(request.body)
|
||||
title = data.get("title")
|
||||
@ -2522,7 +2526,13 @@ def create_estimate(request):
|
||||
item_instance = ItemModel.objects.get(additioinal_info__car_info__hash=items)
|
||||
instance = models.Car.objects.get(hash=item)
|
||||
response = reserve_car(instance, request)
|
||||
|
||||
|
||||
opportunity_id = data.get("opportunity_id")
|
||||
if opportunity_id:
|
||||
opportunity = models.Opportunity.objects.get(pk=int(opportunity_id))
|
||||
opportunity.estimate = estimate
|
||||
opportunity.save()
|
||||
|
||||
url = reverse("estimate_detail", kwargs={"pk": estimate.pk})
|
||||
return JsonResponse(
|
||||
{
|
||||
@ -2536,6 +2546,12 @@ def create_estimate(request):
|
||||
entity_slug=entity.slug, user_model=entity.admin
|
||||
)
|
||||
form.fields["customer"].queryset = entity.get_customers().filter(active=True)
|
||||
|
||||
if pk:
|
||||
opportunity = models.Opportunity.objects.get(pk=pk)
|
||||
customer = opportunity.customer
|
||||
form.initial['customer'] = customer
|
||||
|
||||
car_list = models.Car.objects.filter(dealer=dealer).exclude(status="reserved").annotate(color=F('colors__exterior__rgb'),color_name=F('colors__exterior__name')).values_list(
|
||||
'id_car_make__name', 'id_car_model__name','id_car_serie__name','id_car_trim__name','color','color_name','hash').distinct()
|
||||
context = {
|
||||
@ -2552,6 +2568,7 @@ def create_estimate(request):
|
||||
}
|
||||
for x in car_list
|
||||
],
|
||||
"opportunity_id": opportunity_id
|
||||
}
|
||||
|
||||
return render(request, "sales/estimates/estimate_form.html", context)
|
||||
@ -2567,7 +2584,7 @@ class EstimateDetailView(LoginRequiredMixin, DetailView):
|
||||
if estimate.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
print(finance_data.get("cars"))
|
||||
|
||||
kwargs["data"] = finance_data
|
||||
kwargs["invoice"] = (
|
||||
InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
||||
@ -3147,14 +3164,11 @@ def lead_convert(request, pk):
|
||||
dealer = get_user_type(request)
|
||||
if lead.is_converted:
|
||||
messages.error(request, "Lead is already converted to customer.")
|
||||
return redirect("opportunity_create",pk=lead.pk)
|
||||
if not models.Car.objects.filter(id_car_make=lead.id_car_make,id_car_model=lead.id_car_model).first():
|
||||
messages.error(request, "Cannot convert lead to customer. Car model not found.")
|
||||
return redirect("lead_list")
|
||||
|
||||
lead.convert_to_customer(dealer.entity)
|
||||
messages.success(request, "Lead converted to customer successfully!")
|
||||
return redirect("opportunity_create",pk=lead.pk)
|
||||
else:
|
||||
customer = lead.convert_to_customer(dealer.entity)
|
||||
models.Opportunity.objects.create(dealer=dealer,customer=customer,lead=lead,probability=50,stage=models.Stage.PROSPECT,staff=lead.staff,status=models.Status.QUALIFIED)
|
||||
messages.success(request, "Lead converted to customer successfully!")
|
||||
return redirect("lead_list")
|
||||
|
||||
@login_required
|
||||
def schedule_lead(request, pk):
|
||||
@ -3206,7 +3220,9 @@ def schedule_lead(request, pk):
|
||||
def send_lead_email(request, pk):
|
||||
lead = get_object_or_404(models.Lead, pk=pk)
|
||||
dealer = get_user_type(request)
|
||||
lead.convert_to_customer(dealer.entity)
|
||||
lead.status = models.Status.IN_PROGRESS
|
||||
lead.save()
|
||||
# lead.convert_to_customer(dealer.entity)
|
||||
|
||||
if request.method == "POST":
|
||||
send_email(
|
||||
@ -3307,6 +3323,17 @@ class OpportunityDetailView(DetailView):
|
||||
template_name = "crm/opportunities/opportunity_detail.html"
|
||||
context_object_name = "opportunity"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
form = forms.OpportunityStatusForm()
|
||||
url = reverse("opportunity_update_status", args=[self.object.pk])
|
||||
form.fields["status"].widget.attrs["hx-get"] = url
|
||||
form.fields["stage"].widget.attrs["hx-get"] = url
|
||||
form.fields["status"].initial = self.object.status
|
||||
form.fields["stage"].initial = self.object.stage
|
||||
context["status_form"] = form
|
||||
return context
|
||||
|
||||
|
||||
class OpportunityListView(ListView):
|
||||
model = models.Opportunity
|
||||
@ -3317,8 +3344,7 @@ class OpportunityListView(ListView):
|
||||
def get_queryset(self):
|
||||
dealer = get_user_type(self.request)
|
||||
return models.Opportunity.objects.filter(dealer=dealer).all()
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_opportunity(request, pk):
|
||||
opportunity = get_object_or_404(models.Opportunity, pk=pk)
|
||||
@ -3326,6 +3352,21 @@ def delete_opportunity(request, pk):
|
||||
messages.success(request, _("Opportunity deleted successfully."))
|
||||
return redirect("opportunity_list")
|
||||
|
||||
def opportunity_update_status(request,pk):
|
||||
opportunity = get_object_or_404(models.Opportunity, pk=pk)
|
||||
status = request.GET.get("status")
|
||||
stage = request.GET.get("stage")
|
||||
if status:
|
||||
opportunity.status = status
|
||||
if stage:
|
||||
opportunity.stage = stage
|
||||
opportunity.save()
|
||||
messages.success(request,"Opportunity status updated successfully")
|
||||
# response = HttpResponse(render(request, "crm/opportunities/opportunity_detail.html"),{"opportunity":opportunity})
|
||||
response = HttpResponse(redirect("opportunity_detail",pk=opportunity.pk))
|
||||
response['HX-Refresh'] = 'true'
|
||||
return response
|
||||
# return render(request,"crm/opportunities/opportunity_detail.html",{"opportunity":opportunity})
|
||||
|
||||
# class OpportunityLogsView(LoginRequiredMixin, ListView):
|
||||
# model = models.OpportunityLog
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;400;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link href="{% static 'vendors/simplebar/simplebar.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/sweetalert2.min.css' %}" rel="stylesheet">
|
||||
|
||||
<link href="{% static 'vendors/flatpickr/flatpickr.min.css' %}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css">
|
||||
<link href="{% static 'css/custom.css' %}" rel="stylesheet">
|
||||
@ -57,38 +57,10 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: false,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
Toast.fire({
|
||||
icon: "{{ message.tags }}",
|
||||
titleText: "{{ message| safe }}"});
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
function notify(tag,msg){
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% include "toast-alert.html" %}
|
||||
<main class="main" id="top">
|
||||
{% include 'header.html' %}
|
||||
|
||||
|
||||
<div class="content">
|
||||
{% include "plans/expiration_messages.html" %}
|
||||
{% block period_navigation %}
|
||||
|
||||
@ -79,6 +79,18 @@
|
||||
<span>{{ _("Channel")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
|
||||
<span>{{ _("Stage")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
|
||||
<span>{{ _("Is Opportunity")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;">
|
||||
{{ _("Create date") }}
|
||||
</th>
|
||||
@ -152,6 +164,28 @@
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
|
||||
<td class="align-middle white-space-nowrap fw-semibold text-body-highlight">{{ lead.source|upper }}</td>
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.channel|upper }}</td>
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity.stage == "prospect" %}
|
||||
<span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "proposal" %}
|
||||
<span class="badge text-bg-info">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "negotiation" %}
|
||||
<span class="badge text-bg-warning">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "closed_won" %}
|
||||
<span class="badge text-bg-success">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "closed_lost" %}
|
||||
<span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity %}
|
||||
<a href="{% url 'opportunity_detail' lead.opportunity.id %}">
|
||||
<span class="badge badge-phoenix badge-phoenix-success">View Details <i class="fa-solid fa-arrow-up-right-from-square"></i></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{{ _("No") }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end">
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button
|
||||
@ -169,7 +203,7 @@
|
||||
<a href="{% url 'send_lead_email' lead.id %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a>
|
||||
<a href="{% url 'schedule_lead' lead.id %}" class="dropdown-item text-success-dark">{% trans "Set Schedule" %}</a>
|
||||
{% if not lead.is_converted %}
|
||||
<a href="{% url 'lead_convert' lead.id %}" class="dropdown-item text-success-dark">{% trans "Convert To Customer" %}</a>
|
||||
<a href="{% url 'lead_convert' lead.id %}" class="dropdown-item text-success-dark">{% trans "Convert To Opportunity" %}</a>
|
||||
{% endif %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -69,7 +69,6 @@
|
||||
|
||||
{% for customer in customers %}
|
||||
<!-- Delete Modal -->
|
||||
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td>
|
||||
|
||||
@ -89,7 +88,7 @@
|
||||
{{ customer.address_1 }}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ customer.created|date }}</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
<a href="#" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.pk %}">
|
||||
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.pk %}">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
|
||||
@ -186,7 +186,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<!-- more inner pages-->
|
||||
</li>
|
||||
{% comment %} </li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'opportunity_list' %}">
|
||||
<div class="d-flex align-items-center">
|
||||
@ -194,7 +194,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<!-- more inner pages-->
|
||||
</li>
|
||||
</li> {% endcomment %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -111,7 +111,8 @@
|
||||
customer: document.querySelector('[name=customer]').value,
|
||||
terms: document.querySelector('[name=terms]').value,
|
||||
item: [],
|
||||
quantity: []
|
||||
quantity: [],
|
||||
opportunity_id: "{{opportunity_id}}"
|
||||
};
|
||||
|
||||
|
||||
|
||||
26
templates/toast-alert.html
Normal file
26
templates/toast-alert.html
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="toast-container">
|
||||
<script>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
try {
|
||||
let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: false,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
} });
|
||||
Toast.fire({
|
||||
icon: "{{ message.tags }}",
|
||||
titleText: "{{ message| safe }}"});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
|
||||
</script>
|
||||
</div>
|
||||
Loading…
x
Reference in New Issue
Block a user