update on the lead and opportunity+more
This commit is contained in:
parent
638d3854af
commit
b8079ebf97
@ -16,7 +16,9 @@ from django_ledger.forms.invoice import (
|
||||
)
|
||||
|
||||
from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase
|
||||
from django_ledger.forms.journal_entry import JournalEntryModelCreateForm as JournalEntryModelCreateFormBase
|
||||
from django_ledger.forms.journal_entry import (
|
||||
JournalEntryModelCreateForm as JournalEntryModelCreateFormBase,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Dealer,
|
||||
@ -45,7 +47,7 @@ from .models import (
|
||||
Customer,
|
||||
Organization,
|
||||
DealerSettings,
|
||||
Tasks
|
||||
Tasks,
|
||||
)
|
||||
from django_ledger import models as ledger_models
|
||||
from django.forms import (
|
||||
@ -60,11 +62,12 @@ User = get_user_model()
|
||||
|
||||
class SaudiPhoneNumberField(forms.CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('min_length', 10)
|
||||
kwargs.setdefault('max_length', 13)
|
||||
kwargs.setdefault("min_length", 10)
|
||||
kwargs.setdefault("max_length", 13)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.validators.append(SaudiPhoneNumberValidator())
|
||||
|
||||
|
||||
class AdditionalServiceForm(forms.ModelForm):
|
||||
"""
|
||||
A form used for creating and updating instances of the
|
||||
@ -81,10 +84,12 @@ class AdditionalServiceForm(forms.ModelForm):
|
||||
model that this form includes.
|
||||
:type Meta.fields: list
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = AdditionalServices
|
||||
fields = ["name", "price", "description", "taxable", "uom"]
|
||||
|
||||
|
||||
class StaffForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for managing Staff entities, including associated user email updates
|
||||
@ -100,6 +105,7 @@ class StaffForm(forms.ModelForm):
|
||||
:ivar service_offered: Collection of services offered by the staff.
|
||||
:type service_offered: forms.ModelMultipleChoiceField
|
||||
"""
|
||||
|
||||
email = forms.EmailField(
|
||||
required=True,
|
||||
label=_("Email"),
|
||||
@ -110,16 +116,20 @@ class StaffForm(forms.ModelForm):
|
||||
label=_("Services Offered"),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
|
||||
queryset=Service.objects.all(),
|
||||
required=False,)
|
||||
required=False,
|
||||
)
|
||||
phone_number = SaudiPhoneNumberField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _('Phone Number'),
|
||||
'id': 'phone'
|
||||
}),
|
||||
label=_('Phone Number'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": _("Phone Number"),
|
||||
"id": "phone",
|
||||
}
|
||||
),
|
||||
label=_("Phone Number"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Staff
|
||||
fields = ["name", "arabic_name", "phone_number", "staff_type"]
|
||||
@ -150,7 +160,9 @@ class DealerForm(forms.ModelForm):
|
||||
:type logo: File
|
||||
|
||||
"""
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
class Meta:
|
||||
model = Dealer
|
||||
fields = [
|
||||
@ -165,12 +177,12 @@ class DealerForm(forms.ModelForm):
|
||||
|
||||
|
||||
class CustomerForm(forms.ModelForm):
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = [
|
||||
'title',
|
||||
"title",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
@ -178,19 +190,31 @@ class CustomerForm(forms.ModelForm):
|
||||
"national_id",
|
||||
"dob",
|
||||
"address",
|
||||
'image',
|
||||
"image",
|
||||
]
|
||||
widgets = {
|
||||
'title': forms.Select(attrs={'class': 'form-control form-control-sm'}),
|
||||
'first_name': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'last_name': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'phone_number': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'national_id': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
'dob': DateInput(attrs={'class': 'form-control form-control-sm', 'type': 'date'}),
|
||||
'address': forms.Textarea(attrs={'class': 'form-control form-control-sm'}),
|
||||
'image': forms.FileInput(attrs={'class': 'form-control form-control-sm'}),
|
||||
"title": forms.Select(attrs={"class": "form-control form-control-sm"}),
|
||||
"first_name": forms.TextInput(
|
||||
attrs={"class": "form-control form-control-sm"}
|
||||
),
|
||||
"last_name": forms.TextInput(
|
||||
attrs={"class": "form-control form-control-sm"}
|
||||
),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control form-control-sm"}),
|
||||
"phone_number": forms.TextInput(
|
||||
attrs={"class": "form-control form-control-sm"}
|
||||
),
|
||||
"national_id": forms.TextInput(
|
||||
attrs={"class": "form-control form-control-sm"}
|
||||
),
|
||||
"dob": DateInput(
|
||||
attrs={"class": "form-control form-control-sm", "type": "date"}
|
||||
),
|
||||
"address": forms.Textarea(attrs={"class": "form-control form-control-sm"}),
|
||||
"image": forms.FileInput(attrs={"class": "form-control form-control-sm"}),
|
||||
}
|
||||
|
||||
|
||||
# class CustomerForm(forms.Form):
|
||||
# """
|
||||
# Represents a form for collecting customer information.
|
||||
@ -264,10 +288,21 @@ class OrganizationForm(forms.ModelForm):
|
||||
:ivar logo: Optional field to upload the logo of the organization.
|
||||
:type logo: forms.ImageField
|
||||
"""
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'),required=True)
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"), required=True)
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ["name","arabic_name","email","phone_number","crn","vrn","address","logo"]
|
||||
fields = [
|
||||
"name",
|
||||
"arabic_name",
|
||||
"email",
|
||||
"phone_number",
|
||||
"crn",
|
||||
"vrn",
|
||||
"address",
|
||||
"logo",
|
||||
]
|
||||
|
||||
|
||||
class CarForm(
|
||||
@ -289,6 +324,7 @@ class CarForm(
|
||||
:ivar Meta.widgets: Defines the widgets for specific fields.
|
||||
:type Meta.widgets: dict
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Car
|
||||
fields = [
|
||||
@ -346,6 +382,7 @@ class CarUpdateForm(forms.ModelForm, AddClassMixin):
|
||||
uses a `DateTimeInput` configured with `datetime-local` attributes, while
|
||||
"remarks" uses a `Textarea` with a specific row configuration.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Car
|
||||
fields = [
|
||||
@ -379,6 +416,7 @@ class CarFinanceForm(forms.ModelForm):
|
||||
:ivar additional_finances: A field that allows selecting multiple
|
||||
additional services associated with a car finance application.
|
||||
"""
|
||||
|
||||
additional_finances = forms.ModelMultipleChoiceField(
|
||||
queryset=AdditionalServices.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
|
||||
@ -414,6 +452,7 @@ class CarLocationForm(forms.ModelForm):
|
||||
specified fields, and custom widget configurations.
|
||||
:type Meta: class
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CarLocation
|
||||
fields = ["showroom", "description"]
|
||||
@ -437,6 +476,7 @@ class CarTransferForm(forms.ModelForm):
|
||||
:ivar widgets: Custom widgets applied to the form fields.
|
||||
:type widgets: dict[str, Any]
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CarTransfer
|
||||
fields = ["car", "to_dealer", "remarks"]
|
||||
@ -461,6 +501,7 @@ class CustomCardForm(forms.ModelForm):
|
||||
includes a specific label for localized presentation.
|
||||
:type custom_date: forms.DateTimeField
|
||||
"""
|
||||
|
||||
custom_date = forms.DateTimeField(
|
||||
widget=forms.DateInput(attrs={"type": "date"}),
|
||||
label=_("Custom Date"),
|
||||
@ -490,6 +531,7 @@ class CarRegistrationForm(forms.ModelForm):
|
||||
:ivar Meta.widgets: Custom widget configurations for specific fields.
|
||||
:type Meta.widgets: dict
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CarRegistration
|
||||
fields = ["plate_number", "text1", "text2", "text3", "registration_date"]
|
||||
@ -512,7 +554,8 @@ class VendorForm(forms.ModelForm):
|
||||
:ivar Meta: Inner class to define metadata for the Vendor form.
|
||||
:type Meta: Type[VendorForm.Meta]
|
||||
"""
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
contact_person = forms.CharField(label=_("Contact Person"))
|
||||
|
||||
class Meta:
|
||||
@ -546,30 +589,31 @@ class CarColorsForm(forms.ModelForm):
|
||||
to include in the form, namely ``exterior`` and ``interior``.
|
||||
:type Meta.fields: list
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CarColors
|
||||
fields = ["exterior", "interior"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["exterior"].queryset = ExteriorColors.objects.all()
|
||||
self.fields["exterior"].widget = forms.RadioSelect(
|
||||
attrs={"class": "form-check-input"}
|
||||
)
|
||||
self.fields["exterior"].choices = [
|
||||
(color.id, f"{color.get_local_name}")
|
||||
for color in ExteriorColors.objects.all().order_by("-name")
|
||||
]
|
||||
self.fields["exterior"].queryset = ExteriorColors.objects.all()
|
||||
self.fields["exterior"].widget = forms.RadioSelect(
|
||||
attrs={"class": "form-check-input"}
|
||||
)
|
||||
self.fields["exterior"].choices = [
|
||||
(color.id, f"{color.get_local_name}")
|
||||
for color in ExteriorColors.objects.all().order_by("-name")
|
||||
]
|
||||
|
||||
self.fields["interior"].queryset = InteriorColors.objects.all()
|
||||
self.fields["interior"].widget = forms.RadioSelect(
|
||||
attrs={"class": "form-check-input"}
|
||||
)
|
||||
self.fields["interior"].choices = [
|
||||
(color.id, f"{color.get_local_name}")
|
||||
for color in InteriorColors.objects.all().order_by("-name")
|
||||
]
|
||||
self.fields["interior"].queryset = InteriorColors.objects.all()
|
||||
self.fields["interior"].widget = forms.RadioSelect(
|
||||
attrs={"class": "form-check-input"}
|
||||
)
|
||||
self.fields["interior"].choices = [
|
||||
(color.id, f"{color.get_local_name}")
|
||||
for color in InteriorColors.objects.all().order_by("-name")
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@ -599,7 +643,8 @@ class RepresentativeForm(forms.ModelForm):
|
||||
:ivar Meta.fields: The fields from the model to include in the form.
|
||||
:type Meta.fields: list of str
|
||||
"""
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
class Meta:
|
||||
model = Representative
|
||||
@ -632,6 +677,7 @@ class CarSelectionTable(tables.Table):
|
||||
model, fields, and template.
|
||||
:type Meta: django_tables2.tables.Table.Meta
|
||||
"""
|
||||
|
||||
select = tables.CheckBoxColumn(accessor="pk", orderable=False)
|
||||
|
||||
class Meta:
|
||||
@ -666,8 +712,9 @@ class WizardForm1(forms.Form):
|
||||
This is a required checkbox input.
|
||||
:type terms: django.forms.BooleanField
|
||||
"""
|
||||
|
||||
hx_attrs = {
|
||||
"hx-post":"",
|
||||
"hx-post": "",
|
||||
"hx-target": "#wizardValidationForm1",
|
||||
"hx-select": "#wizardValidationForm1",
|
||||
"hx-trigger": "blur delay:500ms",
|
||||
@ -698,7 +745,7 @@ class WizardForm1(forms.Form):
|
||||
"required": "required",
|
||||
# **hx_attrs
|
||||
},
|
||||
render_value=True
|
||||
render_value=True,
|
||||
),
|
||||
error_messages={
|
||||
"required": _("This field is required."),
|
||||
@ -715,7 +762,7 @@ class WizardForm1(forms.Form):
|
||||
"required": "required",
|
||||
# **hx_attrs
|
||||
},
|
||||
render_value=True
|
||||
render_value=True,
|
||||
),
|
||||
error_messages={
|
||||
"required": _("This field is required."),
|
||||
@ -737,7 +784,6 @@ class WizardForm1(forms.Form):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get("email")
|
||||
if email:
|
||||
@ -777,6 +823,7 @@ class WizardForm2(forms.Form):
|
||||
and region-specific configuration.
|
||||
:type phone_number: PhoneNumberField
|
||||
"""
|
||||
|
||||
name = forms.CharField(
|
||||
label=_("Name"),
|
||||
widget=forms.TextInput(
|
||||
@ -819,7 +866,7 @@ class WizardForm2(forms.Form):
|
||||
# },
|
||||
# required=True,
|
||||
# )
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
|
||||
class WizardForm3(forms.Form):
|
||||
@ -841,6 +888,7 @@ class WizardForm3(forms.Form):
|
||||
with validation enforcing the field to be required.
|
||||
:type address: django.forms.fields.CharField
|
||||
"""
|
||||
|
||||
# CRN field with max length of 10
|
||||
crn = forms.CharField(
|
||||
label=_("CRN"),
|
||||
@ -891,6 +939,7 @@ class WizardForm3(forms.Form):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ItemForm(forms.Form):
|
||||
"""
|
||||
A form for handling item-related inputs in the application.
|
||||
@ -906,6 +955,7 @@ class ItemForm(forms.Form):
|
||||
selected item.
|
||||
:type quantity: DecimalField
|
||||
"""
|
||||
|
||||
item = forms.ModelChoiceField(
|
||||
queryset=ledger_models.ItemModel.objects.all(),
|
||||
label=_("Item"),
|
||||
@ -935,6 +985,7 @@ class PaymentForm(forms.Form):
|
||||
:ivar payment_date: The date on which the payment is made.
|
||||
:type payment_date: DateField
|
||||
"""
|
||||
|
||||
invoice = forms.ModelChoiceField(
|
||||
queryset=ledger_models.InvoiceModel.objects.all(),
|
||||
label=_("Invoice"),
|
||||
@ -994,6 +1045,7 @@ class EmailForm(forms.Form):
|
||||
:ivar to_email: The recipient's email address.
|
||||
:type to_email: forms.EmailField
|
||||
"""
|
||||
|
||||
subject = forms.CharField(max_length=255)
|
||||
message = forms.CharField(widget=forms.Textarea)
|
||||
from_email = forms.EmailField()
|
||||
@ -1016,7 +1068,8 @@ class LeadForm(forms.ModelForm):
|
||||
options are displayed until a car make is selected.
|
||||
:type id_car_model: ModelChoiceField
|
||||
"""
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
# email = forms.EmailField(
|
||||
# label=_("Email"),
|
||||
@ -1064,32 +1117,32 @@ class LeadForm(forms.ModelForm):
|
||||
"last_name",
|
||||
"email",
|
||||
"phone_number",
|
||||
"lead_type",
|
||||
"address",
|
||||
"lead_type",
|
||||
"id_car_make",
|
||||
"id_car_model",
|
||||
"crn",
|
||||
"vrn",
|
||||
"year",
|
||||
"salary",
|
||||
"source",
|
||||
"channel",
|
||||
"staff",
|
||||
"priority",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter_qs(self,**kwargs):
|
||||
dealer = kwargs['dealer']
|
||||
dealer_make_list = DealersMake.objects.filter(dealer=dealer).values_list("car_make",flat=True)
|
||||
def filter_qs(self, **kwargs):
|
||||
dealer = kwargs["dealer"]
|
||||
dealer_make_list = DealersMake.objects.filter(dealer=dealer).values_list(
|
||||
"car_make", flat=True
|
||||
)
|
||||
if "id_car_make" in self.fields:
|
||||
queryset = self.fields["id_car_make"].queryset.filter(is_sa_import=True,pk__in=dealer_make_list)
|
||||
queryset = self.fields["id_car_make"].queryset.filter(
|
||||
is_sa_import=True, pk__in=dealer_make_list
|
||||
)
|
||||
self.fields["id_car_make"].choices = [
|
||||
(obj.id_car_make, obj.get_local_name()) for obj in queryset
|
||||
]
|
||||
|
||||
|
||||
class ScheduleForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for scheduling events, extending ModelForm to bind to the
|
||||
@ -1103,12 +1156,20 @@ class ScheduleForm(forms.ModelForm):
|
||||
'datetime-local' HTML widget.
|
||||
:type scheduled_at: DateTimeField
|
||||
"""
|
||||
|
||||
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):
|
||||
@ -1125,6 +1186,7 @@ class NoteForm(forms.ModelForm):
|
||||
:ivar Meta.fields: The fields to include in the form.
|
||||
:type Meta.fields: list
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Notes
|
||||
fields = ["note"]
|
||||
@ -1143,7 +1205,11 @@ class ActivityForm(forms.ModelForm):
|
||||
associated with the form and the fields it comprises.
|
||||
:type Meta: type
|
||||
"""
|
||||
activity_type = forms.ChoiceField(choices=[("call", "Call"), ("email", "Email"), ("meeting", "Meeting")])
|
||||
|
||||
activity_type = forms.ChoiceField(
|
||||
choices=[("call", "Call"), ("email", "Email"), ("meeting", "Meeting")]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = ["activity_type", "notes"]
|
||||
@ -1164,34 +1230,48 @@ class OpportunityForm(forms.ModelForm):
|
||||
:ivar Meta.fields: List of fields from the model included in the form.
|
||||
:type Meta.fields: list
|
||||
"""
|
||||
closing_date = forms.DateField(
|
||||
label=_("Expected Closing Date"),
|
||||
widget=forms.DateInput(attrs={"type": "date"})
|
||||
|
||||
expected_close_date = forms.DateField(
|
||||
label=_("Expected Closing Date"), widget=forms.DateInput(attrs={"type": "date"})
|
||||
)
|
||||
|
||||
probability = forms.IntegerField(
|
||||
label=_("Probability (%)"),
|
||||
widget=forms.NumberInput(attrs={
|
||||
'type': 'range',
|
||||
'min': '0',
|
||||
'max': '100',
|
||||
'step': '1',
|
||||
'class': 'form-range',
|
||||
'oninput': 'this.nextElementSibling.value = this.value'
|
||||
}),
|
||||
initial=50 # Default value
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"type": "range",
|
||||
"min": "0",
|
||||
"max": "100",
|
||||
"step": "1",
|
||||
"class": "form-range",
|
||||
"oninput": "this.nextElementSibling.value = this.value",
|
||||
}
|
||||
),
|
||||
initial=50, # Default value
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Opportunity
|
||||
fields = ["lead", "car", "stage", "probability", "expected_revenue", "closing_date"]
|
||||
|
||||
fields = [
|
||||
"lead",
|
||||
"car",
|
||||
"stage",
|
||||
"probability",
|
||||
"amount",
|
||||
"expected_revenue",
|
||||
"expected_close_date",
|
||||
]
|
||||
widgets = {
|
||||
"expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}),
|
||||
}
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Add a visible number input to display the current value
|
||||
self.fields['probability'].widget.attrs['class'] = 'd-none' # Hide the default input
|
||||
self.fields["probability"].widget.attrs["class"] = (
|
||||
"d-none" # Hide the default input
|
||||
)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields['probability'].initial = self.instance.probability
|
||||
self.fields["probability"].initial = self.instance.probability
|
||||
|
||||
|
||||
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
||||
@ -1207,6 +1287,7 @@ class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
||||
the field names and values are their respective Field objects.
|
||||
:type fields: dict
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@ -1237,6 +1318,7 @@ class BillModelCreateForm(BillModelCreateFormBase):
|
||||
:ivar fields: A dictionary of all fields used in the form.
|
||||
:type fields: dict
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@ -1244,6 +1326,7 @@ class BillModelCreateForm(BillModelCreateFormBase):
|
||||
self.fields["prepaid_account"].widget = forms.HiddenInput()
|
||||
self.fields["unearned_account"].widget = forms.HiddenInput()
|
||||
|
||||
|
||||
class SaleOrderForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for creating or updating sales orders.
|
||||
@ -1261,11 +1344,23 @@ class SaleOrderForm(forms.ModelForm):
|
||||
for fields, such as customizing their appearance.
|
||||
:type Meta.widgets: Dict[str, Any]
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SaleOrder
|
||||
fields = ["estimate", "payment_method", "comments"]
|
||||
fields = [
|
||||
"estimate",
|
||||
"payment_method",
|
||||
"opportunity",
|
||||
"agreed_price",
|
||||
"down_payment_amount",
|
||||
"loan_amount",
|
||||
"expected_delivery_date",
|
||||
"comments",
|
||||
"status"
|
||||
]
|
||||
widgets = {
|
||||
"comments": forms.Textarea(attrs={"rows": 3}),
|
||||
"expected_delivery_date": forms.DateInput(attrs={"type": "date"}),
|
||||
}
|
||||
|
||||
|
||||
@ -1273,6 +1368,7 @@ class EstimateModelCreateForm(forms.Form):
|
||||
title = forms.CharField(max_length=255)
|
||||
customer = forms.ModelChoiceField(queryset=Customer.objects.none())
|
||||
|
||||
|
||||
# class EstimateModelCreateForm(EstimateModelCreateFormBase):
|
||||
# """
|
||||
# Defines the EstimateModelCreateForm class, which is used to create and manage
|
||||
@ -1358,6 +1454,7 @@ class OpportunityStatusForm(forms.Form):
|
||||
widget configured with attributes to handle UI updates dynamically.
|
||||
:type stage: ChoiceField
|
||||
"""
|
||||
|
||||
status = forms.ChoiceField(
|
||||
label=_("Status"),
|
||||
choices=Status.choices,
|
||||
@ -1390,6 +1487,7 @@ class OpportunityStatusForm(forms.Form):
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
class GroupForm(forms.ModelForm):
|
||||
"""
|
||||
A form for creating and updating CustomGroup objects.
|
||||
@ -1404,10 +1502,12 @@ class GroupForm(forms.ModelForm):
|
||||
:ivar fields: List of fields to include in the form. In this case, only the `name` field.
|
||||
:type fields: List[str]
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = CustomGroup
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class PermissionForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for managing permissions using a multiple-choice field.
|
||||
@ -1421,19 +1521,33 @@ class PermissionForm(forms.ModelForm):
|
||||
related to specific app labels (`inventory` and `django_ledger`).
|
||||
:type name: ModelMultipleChoiceField
|
||||
"""
|
||||
|
||||
name = forms.ModelMultipleChoiceField(
|
||||
queryset=cache.get('permissions_queryset', Permission.objects.filter(content_type__app_label__in=["inventory","django_ledger"])),
|
||||
queryset=cache.get(
|
||||
"permissions_queryset",
|
||||
Permission.objects.filter(
|
||||
content_type__app_label__in=["inventory", "django_ledger"]
|
||||
),
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
cache.set('permissions_queryset', Permission.objects.filter(content_type__app_label__in=["inventory","django_ledger"]), 60*60)
|
||||
cache.set(
|
||||
"permissions_queryset",
|
||||
Permission.objects.filter(
|
||||
content_type__app_label__in=["inventory", "django_ledger"]
|
||||
),
|
||||
60 * 60,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class UserGroupForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for selecting user groups.
|
||||
@ -1446,15 +1560,18 @@ class UserGroupForm(forms.ModelForm):
|
||||
:ivar name: A field for selecting multiple groups using checkboxes.
|
||||
:type name: ModelMultipleChoiceField
|
||||
"""
|
||||
|
||||
name = forms.ModelMultipleChoiceField(
|
||||
queryset= CustomGroup.objects.all(),
|
||||
queryset=CustomGroup.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomGroup
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class DealerSettingsForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for managing DealerSettings.
|
||||
@ -1469,10 +1586,12 @@ class DealerSettingsForm(forms.ModelForm):
|
||||
that all model fields should be included.
|
||||
:type fields: str
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = DealerSettings
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LeadTransferForm(forms.Form):
|
||||
"""
|
||||
Represents a form for transferring leads between staff members.
|
||||
@ -1485,7 +1604,8 @@ class LeadTransferForm(forms.Form):
|
||||
:ivar transfer_to: Field for selecting the staff to transfer the lead to.
|
||||
:type transfer_to: ModelChoiceField
|
||||
"""
|
||||
transfer_to = forms.ModelChoiceField(label="to",queryset=Staff.objects.all())
|
||||
|
||||
transfer_to = forms.ModelChoiceField(label="to", queryset=Staff.objects.all())
|
||||
|
||||
|
||||
class DealersMakeForm(forms.Form):
|
||||
@ -1500,11 +1620,12 @@ class DealersMakeForm(forms.Form):
|
||||
is used to filter and save car makes for a specific dealer.
|
||||
:type dealer: Dealer or None
|
||||
"""
|
||||
|
||||
car_makes = forms.ModelMultipleChoiceField(
|
||||
queryset=CarMake.objects.filter(is_sa_import=True),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={"class": "car-makes-grid"}),
|
||||
required=True,
|
||||
label=_("Select Car Makes")
|
||||
label=_("Select Car Makes"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1532,6 +1653,7 @@ class JournalEntryModelCreateForm(JournalEntryModelCreateFormBase):
|
||||
:ivar bar: Description of bar attribute.
|
||||
:type bar: int
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@ -1549,10 +1671,10 @@ class PlanPricingForm(forms.ModelForm):
|
||||
that all model fields should be included.
|
||||
:type fields: str
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = PlanPricing
|
||||
fields = ["plan","pricing", "price"]
|
||||
|
||||
fields = ["plan", "pricing", "price"]
|
||||
|
||||
|
||||
class CreditCardField(forms.CharField):
|
||||
@ -1560,31 +1682,34 @@ class CreditCardField(forms.CharField):
|
||||
value = super().clean(value)
|
||||
if value:
|
||||
# Remove all non-digit characters
|
||||
cleaned_value = ''.join(c for c in value if c.isdigit())
|
||||
cleaned_value = "".join(c for c in value if c.isdigit())
|
||||
|
||||
# Validate using Luhn algorithm
|
||||
if not Luhn.check_luhn(cleaned_value):
|
||||
raise forms.ValidationError(_("Please enter a valid credit card number"))
|
||||
raise forms.ValidationError(
|
||||
_("Please enter a valid credit card number")
|
||||
)
|
||||
|
||||
# Add basic card type detection (optional)
|
||||
if cleaned_value.startswith('4'):
|
||||
self.card_type = 'visa'
|
||||
elif cleaned_value.startswith(('51', '52', '53', '54', '55')):
|
||||
self.card_type = 'mastercard'
|
||||
elif cleaned_value.startswith(('34', '37')):
|
||||
self.card_type = 'amex'
|
||||
if cleaned_value.startswith("4"):
|
||||
self.card_type = "visa"
|
||||
elif cleaned_value.startswith(("51", "52", "53", "54", "55")):
|
||||
self.card_type = "mastercard"
|
||||
elif cleaned_value.startswith(("34", "37")):
|
||||
self.card_type = "amex"
|
||||
else:
|
||||
self.card_type = 'unknown'
|
||||
self.card_type = "unknown"
|
||||
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class ExpiryDateField(forms.CharField):
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
if value:
|
||||
try:
|
||||
month, year = value.split('/')
|
||||
month, year = value.split("/")
|
||||
month = int(month.strip())
|
||||
year = int(year.strip())
|
||||
|
||||
@ -1600,14 +1725,19 @@ class ExpiryDateField(forms.CharField):
|
||||
current_year = datetime.now().year
|
||||
current_month = datetime.now().month
|
||||
|
||||
if year < current_year or (year == current_year and month < current_month):
|
||||
if year < current_year or (
|
||||
year == current_year and month < current_month
|
||||
):
|
||||
raise forms.ValidationError(_("This card appears to be expired"))
|
||||
|
||||
except (ValueError, AttributeError):
|
||||
raise forms.ValidationError(_("Please enter a valid expiry date in MM/YY format"))
|
||||
raise forms.ValidationError(
|
||||
_("Please enter a valid expiry date in MM/YY format")
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class CVVField(forms.CharField):
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
@ -1618,121 +1748,126 @@ class CVVField(forms.CharField):
|
||||
raise forms.ValidationError(_("CVV must be 3 or 4 digits"))
|
||||
return value
|
||||
|
||||
|
||||
class PaymentPlanForm(forms.Form):
|
||||
# Customer Information
|
||||
first_name = forms.CharField(
|
||||
max_length=100,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _('First Name'),
|
||||
'id': 'first-name'
|
||||
}),
|
||||
label=_('First Name')
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": _("First Name"),
|
||||
"id": "first-name",
|
||||
}
|
||||
),
|
||||
label=_("First Name"),
|
||||
)
|
||||
|
||||
last_name = forms.CharField(
|
||||
max_length=100,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _('Last Name'),
|
||||
'id': 'last-name'
|
||||
}),
|
||||
label=_('Last Name')
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": _("Last Name"),
|
||||
"id": "last-name",
|
||||
}
|
||||
),
|
||||
label=_("Last Name"),
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
widget=forms.EmailInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _('Email'),
|
||||
'id': 'email'
|
||||
}),
|
||||
label=_('Email Address')
|
||||
widget=forms.EmailInput(
|
||||
attrs={"class": "form-control", "placeholder": _("Email"), "id": "email"}
|
||||
),
|
||||
label=_("Email Address"),
|
||||
)
|
||||
|
||||
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
|
||||
phone_number = SaudiPhoneNumberField(label=_("Phone Number"))
|
||||
|
||||
# Credit Card Fields (not saved to database)
|
||||
card_number = CreditCardField(
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '1234 5678 9012 3456',
|
||||
'id': 'card-number',
|
||||
|
||||
}),
|
||||
label=_("Card Number")
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "1234 5678 9012 3456",
|
||||
"id": "card-number",
|
||||
}
|
||||
),
|
||||
label=_("Card Number"),
|
||||
)
|
||||
|
||||
expiry_date = ExpiryDateField(
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'MM/YY',
|
||||
'id': 'expiry',
|
||||
|
||||
}),
|
||||
label=_("Expiration Date")
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "MM/YY",
|
||||
"id": "expiry",
|
||||
}
|
||||
),
|
||||
label=_("Expiration Date"),
|
||||
)
|
||||
|
||||
cvv = CVVField(
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': '123',
|
||||
'id': 'cvv',
|
||||
|
||||
}),
|
||||
label=_("Security Code (CVV)")
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "123",
|
||||
"id": "cvv",
|
||||
}
|
||||
),
|
||||
label=_("Security Code (CVV)"),
|
||||
)
|
||||
|
||||
card_name = forms.CharField(
|
||||
required=True,
|
||||
max_length=100,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'John Doe',
|
||||
'id': 'card-name',
|
||||
|
||||
}),
|
||||
label=_("Name on Card")
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "John Doe",
|
||||
"id": "card-name",
|
||||
}
|
||||
),
|
||||
label=_("Name on Card"),
|
||||
)
|
||||
|
||||
# Terms and conditions
|
||||
terms = forms.BooleanField(
|
||||
required=True,
|
||||
widget=forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input',
|
||||
'id': 'terms'
|
||||
}),
|
||||
label=_('I agree to the Terms and Conditions'),
|
||||
error_messages={
|
||||
'required': _('You must accept the terms and conditions')
|
||||
}
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input", "id": "terms"}),
|
||||
label=_("I agree to the Terms and Conditions"),
|
||||
error_messages={"required": _("You must accept the terms and conditions")},
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
payment_method = self.data.get('payment-method')
|
||||
payment_method = self.data.get("payment-method")
|
||||
|
||||
if payment_method == 'credit-card':
|
||||
if not all([
|
||||
cleaned_data.get('card_number'),
|
||||
cleaned_data.get('expiry_date'),
|
||||
cleaned_data.get('cvv'),
|
||||
cleaned_data.get('card_name')
|
||||
]):
|
||||
if payment_method == "credit-card":
|
||||
if not all(
|
||||
[
|
||||
cleaned_data.get("card_number"),
|
||||
cleaned_data.get("expiry_date"),
|
||||
cleaned_data.get("cvv"),
|
||||
cleaned_data.get("card_name"),
|
||||
]
|
||||
):
|
||||
raise forms.ValidationError("Please complete all credit card fields")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user and user.is_authenticated:
|
||||
# Pre-fill form with user data if available
|
||||
self.fields['first_name'].initial = user.first_name
|
||||
self.fields['last_name'].initial = user.last_name
|
||||
self.fields['email'].initial = user.email
|
||||
self.fields["first_name"].initial = user.first_name
|
||||
self.fields["last_name"].initial = user.last_name
|
||||
self.fields["email"].initial = user.email
|
||||
|
||||
|
||||
# class ActivityHistoryForm(forms.Form):
|
||||
@ -1756,10 +1891,11 @@ class PaymentPlanForm(forms.Form):
|
||||
# label=_('Description')
|
||||
# )
|
||||
|
||||
|
||||
class StaffTaskForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Tasks
|
||||
fields = ['title','due_date' ,'description']
|
||||
fields = ["title", "due_date", "description"]
|
||||
widgets = {
|
||||
'due_date': forms.DateTimeInput(attrs={'type': 'date'}),
|
||||
}
|
||||
"due_date": forms.DateTimeInput(attrs={"type": "date"}),
|
||||
}
|
||||
|
||||
125
inventory/migrations/0003_alter_saleorder_options_and_more.py
Normal file
125
inventory/migrations/0003_alter_saleorder_options_and_more.py
Normal file
@ -0,0 +1,125 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-27 14:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0002_carfinance_is_sold'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='saleorder',
|
||||
options={'ordering': ['-order_date'], 'verbose_name': 'Sales Order', 'verbose_name_plural': 'Sales Orders'},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='saleorder',
|
||||
old_name='created',
|
||||
new_name='created_at',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='actual_delivery_date',
|
||||
field=models.DateTimeField(blank=True, help_text='The actual date and time the vehicle was delivered.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='agreed_price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, help_text='The final agreed-upon selling price of the vehicle.', max_digits=12),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='cancellation_reason',
|
||||
field=models.TextField(blank=True, help_text='Reason for cancellation, if applicable.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='cancelled_date',
|
||||
field=models.DateTimeField(blank=True, help_text='The date and time the order was cancelled, if applicable.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='car',
|
||||
field=models.ForeignKey(default=1, help_text='The specific vehicle (VIN) being sold.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='customer',
|
||||
field=models.ForeignKey(default=1, help_text='The customer making the purchase.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.customer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='down_payment_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The initial payment made by the customer.', max_digits=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='expected_delivery_date',
|
||||
field=models.DateField(blank=True, help_text='The planned date for vehicle delivery.', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='last_modified_by',
|
||||
field=models.ForeignKey(help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='loan_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The amount financed by a bank or third-party lender.', max_digits=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='opportunity',
|
||||
field=models.OneToOneField(default=1, help_text='The associated sales opportunity for this order.', on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.opportunity'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='order_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time the sales order was created.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='remaining_balance',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The remaining amount due from the customer or financing.', max_digits=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('IN_FINANCING', 'In Financing'), ('PARTIALLY_PAID', 'Partially Paid'), ('FULLY_PAID', 'Fully Paid'), ('PENDING_DELIVERY', 'Pending Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled')], default='PENDING_APPROVAL', help_text='Current status of the sales order.', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='total_paid_amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, help_text='Sum of down payment, trade-in value, and loan amount received so far.', max_digits=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='trade_in_value',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The value of any vehicle traded in by the customer.', max_digits=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='trade_in_vehicle',
|
||||
field=models.ForeignKey(blank=True, help_text='The vehicle traded in by the customer, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='traded_in_on_orders', to='inventory.car'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0004_alter_saleorder_car.py
Normal file
19
inventory/migrations/0004_alter_saleorder_car.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-27 14:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0003_alter_saleorder_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='saleorder',
|
||||
name='car',
|
||||
field=models.ForeignKey(blank=True, help_text='The specific vehicle (VIN) being sold.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0005_alter_opportunity_stage.py
Normal file
18
inventory/migrations/0005_alter_opportunity_stage.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-28 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_alter_saleorder_car'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='stage',
|
||||
field=models.CharField(choices=[('qualification', 'Qualification'), ('test_drive', 'Test Drive'), ('quotation', 'Quotation'), ('negotiation', 'Negotiation'), ('financing', 'Financing'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ('on_hold', 'On Hold')], max_length=20, verbose_name='Stage'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-28 13:16
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_alter_opportunity_stage'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='closing_date',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='assigned_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_opportunities', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='expected_close_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='loss_reason',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='vehicle_of_interest_make',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='vehicle_of_interest_model',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-28 13:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0006_remove_opportunity_closing_date_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], db_index=True, default='new', max_length=50, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='new_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='old_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='Old Status'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-28 13:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0007_alter_lead_status_alter_leadstatushistory_new_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='address',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='crn',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='priority',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='salary',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='vrn',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='lead',
|
||||
name='year',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-28 13:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_remove_lead_address_remove_lead_crn_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='address',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Address'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='crn',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='CRN'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='priority',
|
||||
field=models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='medium', max_length=20, verbose_name='Priority'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='salary',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Salary'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='vrn',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='VRN'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0010_alter_opportunity_assigned_to.py
Normal file
19
inventory/migrations/0010_alter_opportunity_assigned_to.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-29 15:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0009_lead_address_opportunity_crn_opportunity_priority_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='assigned_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_opportunities', to='inventory.staff'),
|
||||
),
|
||||
]
|
||||
17
inventory/migrations/0011_remove_opportunity_assigned_to.py
Normal file
17
inventory/migrations/0011_remove_opportunity_assigned_to.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-29 15:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0010_alter_opportunity_assigned_to'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='assigned_to',
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0012_opportunity_organization.py
Normal file
19
inventory/migrations/0012_opportunity_organization.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-29 16:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0011_remove_opportunity_assigned_to'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='organization',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.organization', verbose_name='Organization'),
|
||||
),
|
||||
]
|
||||
23
inventory/migrations/0013_opportunity_amount_and_more.py
Normal file
23
inventory/migrations/0013_opportunity_amount_and_more.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-29 23:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0012_opportunity_organization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='amount',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Amount'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='expected_revenue',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Expected Revenue'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0014_alter_opportunity_amount.py
Normal file
19
inventory/migrations/0014_alter_opportunity_amount.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-29 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0013_opportunity_amount_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Amount'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
1017
inventory/models.py
1017
inventory/models.py
File diff suppressed because it is too large
Load Diff
@ -125,7 +125,7 @@ urlpatterns = [
|
||||
name="add_note",
|
||||
),
|
||||
path(
|
||||
"crm/<int:pk>/update-task/",
|
||||
"crm/<str:content_type>/<int:pk>/update-task/",
|
||||
views.update_task,
|
||||
name="update_task",
|
||||
),
|
||||
@ -598,6 +598,7 @@ path(
|
||||
"sales/estimates/<uuid:pk>/send_email", views.send_email_view, name="send_email"
|
||||
),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/', views.create_sale_order, name='create_sale_order'),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/', views.SaleOrderDetail.as_view(), name='sale_order_details'),
|
||||
path('sales/estimates/<uuid:pk>/sale_order/preview/', views.preview_sale_order, name='preview_sale_order'),
|
||||
|
||||
# Invoice
|
||||
|
||||
@ -27,6 +27,11 @@ from django.utils.translation import get_language
|
||||
from appointment.models import StaffMember
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import secrets
|
||||
|
||||
def make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
|
||||
return ''.join(secrets.choice(allowed_chars) for i in range(length))
|
||||
|
||||
def get_jwt_token():
|
||||
"""
|
||||
Fetches a JWT token from an external authentication API.
|
||||
@ -999,13 +1004,13 @@ class CarFinanceCalculator:
|
||||
car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY)
|
||||
car_info = self._get_nested_value(item, self.CAR_INFO_KEY)
|
||||
unit_price = Decimal(car_finance.get('selling_price', 0))
|
||||
|
||||
return {
|
||||
"item_number": item.item_model.item_number,
|
||||
"vin": car_info.get('vin'),
|
||||
"make": car_info.get('make'),
|
||||
"model": car_info.get('model'),
|
||||
"year": car_info.get('year'),
|
||||
"logo": item.item_model.car.id_car_make.logo.url,
|
||||
"trim": car_info.get('trim'),
|
||||
"mileage": car_info.get('mileage'),
|
||||
"cost_price": car_finance.get('cost_price'),
|
||||
|
||||
@ -3912,7 +3912,13 @@ def create_sale_order(request, pk):
|
||||
if request.method == "POST":
|
||||
form = forms.SaleOrderForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
instance = form.save(commit=False)
|
||||
instance.estimate = estimate
|
||||
instance.customer = estimate.customer.customer_set.first()
|
||||
instance.created_by = request.user
|
||||
instance.last_modified_by = request.user
|
||||
instance.save()
|
||||
|
||||
if not estimate.is_approved():
|
||||
estimate.mark_as_approved()
|
||||
estimate.save()
|
||||
@ -3925,8 +3931,11 @@ def create_sale_order(request, pk):
|
||||
dealer = get_user_type(request)
|
||||
|
||||
item.item_model.car.mark_as_sold()
|
||||
# models.Activity.objects.create(dealer=dealer,content_object=item.item_model.car, notes="Car Sold",created_by=request.user,activity_type=models.ActionChoices.SALE_CAR)
|
||||
|
||||
return redirect("estimate_detail", pk=estimate.pk)
|
||||
# models.Activity.objects.create(dealer=dealer,content_object=item.item_model.car, notes="Car Sold",created_by=request.user,activity_type=models.ActionChoices.SALE_CAR)
|
||||
else:
|
||||
print(form.errors)
|
||||
|
||||
messages.success(request, "Sale Order created successfully")
|
||||
return redirect("estimate_detail", pk=estimate.pk)
|
||||
@ -3938,10 +3947,30 @@ def create_sale_order(request, pk):
|
||||
finance_data = calculator.get_finance_data()
|
||||
return render(
|
||||
request,
|
||||
"sales/estimates/sale_order_form.html",
|
||||
"sales/estimates/sale_order_form1.html",
|
||||
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
|
||||
)
|
||||
|
||||
class SaleOrderDetail(DetailView):
|
||||
model = models.SaleOrder
|
||||
template_name = "sales/orders/order_details.html"
|
||||
context_object_name = "saleorder"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
order_pk = self.kwargs.get('order_pk')
|
||||
return models.SaleOrder.objects.get(
|
||||
pk=order_pk,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
saleorder = kwargs.get("object")
|
||||
estimate = saleorder.estimate
|
||||
if estimate.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
kwargs["data"] = finance_data
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
def preview_sale_order(request, pk):
|
||||
@ -4408,9 +4437,12 @@ def invoice_create(request, pk):
|
||||
commit=True,
|
||||
operation=InvoiceModel.ITEMIZE_APPEND,
|
||||
)
|
||||
sale_order = estimate.sale_orders.first()
|
||||
sale_order.invoice = invoice
|
||||
invoice.bind_estimate(estimate)
|
||||
invoice.mark_as_review()
|
||||
estimate.mark_as_completed()
|
||||
sale_order.save()
|
||||
estimate.save()
|
||||
invoice.save()
|
||||
messages.success(request, "Invoice created successfully")
|
||||
@ -4730,7 +4762,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
query = self.request.GET.get("q")
|
||||
dealer = get_user_type(self.request)
|
||||
qs = models.Lead.objects.filter(dealer=dealer)
|
||||
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
|
||||
if query:
|
||||
qs = apply_search_filters(qs, query)
|
||||
if self.request.is_dealer:
|
||||
@ -4834,9 +4866,10 @@ def lead_create(request):
|
||||
email=instance.email,
|
||||
first_name=instance.first_name,
|
||||
last_name=instance.last_name,
|
||||
active=False,
|
||||
)
|
||||
customer.create_user_model()
|
||||
customer.create_customer_model()
|
||||
customer.create_user_model(for_lead=True)
|
||||
customer.create_customer_model(for_lead=True)
|
||||
customer.save()
|
||||
instance.customer = customer
|
||||
|
||||
@ -4851,12 +4884,13 @@ def lead_create(request):
|
||||
phone_number=instance.phone_number,
|
||||
email=instance.email,
|
||||
name=instance.first_name + " " + instance.last_name,
|
||||
active=False,
|
||||
)
|
||||
organization.create_user_model()
|
||||
organization.create_customer_model()
|
||||
organization.create_user_model(for_lead=True)
|
||||
organization.create_customer_model(for_lead=True)
|
||||
organization.save()
|
||||
instance.organization = organization
|
||||
instance.next_action = LeadStatus.FOLLOW_UP
|
||||
instance.next_action = LeadStatus.NEW
|
||||
instance.save()
|
||||
messages.success(request, _("Lead created successfully"))
|
||||
return redirect("lead_list")
|
||||
@ -4926,7 +4960,7 @@ def update_lead_actions(request):
|
||||
current_action = request.POST.get("current_action")
|
||||
next_action = request.POST.get("next_action")
|
||||
next_action_date = request.POST.get("next_action_date", None)
|
||||
print(request.POST)
|
||||
|
||||
if not all([lead_id, current_action, next_action]):
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "All fields are required"}, status=400
|
||||
@ -5076,12 +5110,14 @@ def add_note_to_opportunity(request, slug):
|
||||
:return: A redirect response to the detailed view of the opportunity.
|
||||
"""
|
||||
opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
||||
dealer = get_user_type(request)
|
||||
if request.method == "POST":
|
||||
notes = request.POST.get("notes")
|
||||
if not notes:
|
||||
messages.error(request, _("Notes field is required"))
|
||||
else:
|
||||
models.Notes.objects.create(
|
||||
dealer=dealer,
|
||||
content_object=opportunity, created_by=request.user, note=notes
|
||||
)
|
||||
messages.success(request, _("Note added successfully"))
|
||||
@ -5426,14 +5462,20 @@ class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin)
|
||||
if self.kwargs.get("slug", None):
|
||||
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
|
||||
initial["lead"] = lead
|
||||
initial["stage"] = models.Stage.PROPOSAL
|
||||
initial["stage"] = models.Stage.QUALIFICATION
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.dealer = dealer
|
||||
form.instance.customer = form.instance.lead.customer
|
||||
form.instance.staff = form.instance.lead.staff
|
||||
instance = form.save(commit=False)
|
||||
if self.kwargs.get("slug"):
|
||||
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
|
||||
instance.dealer = dealer
|
||||
instance.staff = lead.staff
|
||||
instance.lead = lead
|
||||
lead.convert_to_customer()
|
||||
lead.save()
|
||||
instance.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -5501,12 +5543,21 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
|
||||
form.fields["stage"].widget.attrs["hx-get"] = url
|
||||
form.fields["stage"].initial = self.object.stage
|
||||
context["status_form"] = form
|
||||
context["lead_notes"] = models.Notes.objects.filter(
|
||||
content_type__model="lead", object_id=self.object.id
|
||||
).order_by("-created")
|
||||
context["notes"] = models.Notes.objects.filter(
|
||||
content_type__model="opportunity", object_id=self.object.id
|
||||
).order_by("-created")
|
||||
context["lead_activities"] = models.Activity.objects.filter(
|
||||
content_type__model="lead", object_id=self.object.id
|
||||
)
|
||||
context["activities"] = models.Activity.objects.filter(
|
||||
content_type__model="opportunity", object_id=self.object.id
|
||||
)
|
||||
lead_email_qs = models.Email.objects.filter(
|
||||
content_type__model="lead", object_id=self.object.id
|
||||
)
|
||||
email_qs = models.Email.objects.filter(
|
||||
content_type__model="opportunity", object_id=self.object.id
|
||||
)
|
||||
@ -5514,6 +5565,17 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
|
||||
"sent": email_qs.filter(status="SENT"),
|
||||
"draft": email_qs.filter(status="DRAFT"),
|
||||
}
|
||||
context["lead_emails"] = {
|
||||
"sent": lead_email_qs.filter(status="SENT"),
|
||||
"draft": lead_email_qs.filter(status="DRAFT"),
|
||||
}
|
||||
context["staff_task_form"] = forms.StaffTaskForm()
|
||||
context["lead_tasks"] = models.Tasks.objects.filter(
|
||||
content_type__model="lead", object_id=self.object.id
|
||||
)
|
||||
context["tasks"] = models.Tasks.objects.filter(
|
||||
content_type__model="opportunity", object_id=self.object.id
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@ -8082,21 +8144,22 @@ def add_task(request, content_type, slug):
|
||||
return redirect(f"{content_type}_detail", slug=slug)
|
||||
|
||||
|
||||
def update_task(request, pk):
|
||||
def update_task(request,content_type, pk):
|
||||
try:
|
||||
model = apps.get_model(f"inventory.{content_type}")
|
||||
except LookupError:
|
||||
raise Http404("Model not found")
|
||||
|
||||
task = get_object_or_404(models.Tasks, pk=pk)
|
||||
lead = get_object_or_404(models.Lead, pk=task.content_object.id)
|
||||
obj = get_object_or_404(model, pk=task.content_object.id)
|
||||
|
||||
if request.method == "POST":
|
||||
task.completed = False if task.completed else True
|
||||
task.save()
|
||||
messages.success(request, _("Task updated successfully"))
|
||||
else:
|
||||
messages.error(request, _("Task form is not valid"))
|
||||
# response = HttpResponse()
|
||||
# response['HX-Refresh'] = 'true'
|
||||
# return response
|
||||
tasks = models.Tasks.objects.filter(content_type__model="lead", object_id=lead.id)
|
||||
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "tasks": tasks})
|
||||
|
||||
# tasks = models.Tasks.objects.filter(content_type__model=content_type, object_id=obj.id)
|
||||
|
||||
return render(request, "partials/task.html", {"task": task})
|
||||
|
||||
|
||||
def add_note(request, content_type, slug):
|
||||
|
||||
21
templates/components/task_modal.html
Normal file
21
templates/components/task_modal.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
<!-- task Modal -->
|
||||
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="taskModalLabel">{% trans 'Task' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_task' content_type slug %}" method="post" class="add_task_form">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38,60 +38,7 @@
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="modal fade" id="actionTrackingModal" tabindex="-1" aria-labelledby="actionTrackingModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="actionTrackingModalLabel">{{ _("Update Lead Actions") }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="actionTrackingForm" method="post">
|
||||
<div class="modal-body">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="leadId" name="lead_id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="currentAction" class="form-label">{{ _("Current Action") }}</label>
|
||||
<select class="form-select" id="currentAction" name="current_action" required>
|
||||
<option value="">{{ _("Select Action") }}</option>
|
||||
<option value="follow_up">{{ _("Follow Up") }}</option>
|
||||
<option value="negotiation">{{ _("Negotiation") }}</option>
|
||||
<option value="won">{{ _("Won") }}</option>
|
||||
<option value="lost">{{ _("Lost") }}</option>
|
||||
<option value="closed">{{ _("Closed") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextAction" class="form-label">{{ _("Next Action") }}</label>
|
||||
<select class="form-select" id="nextAction" name="next_action" required>
|
||||
<option value="">{{ _("Select Next Action") }}</option>
|
||||
<option value="no_action">{{ _("No Action") }}</option>
|
||||
<option value="call">{{ _("Call") }}</option>
|
||||
<option value="meeting">{{ _("Meeting") }}</option>
|
||||
<option value="email">{{ _("Email") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextActionDate" class="form-label">{{ _("Next Action Date") }}</label>
|
||||
<input type="datetime-local" class="form-control" id="nextActionDate" name="next_action_date">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="actionNotes" class="form-label">{{ _("Notes") }}</label>
|
||||
<textarea class="form-control" id="actionNotes" name="action_notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "crm/leads/partials/update_action.html" %}
|
||||
<div class="row align-items-center justify-content-between g-3 mb-3">
|
||||
<div class="col-12 col-md-auto">
|
||||
<h4 class="mb-0">{{ _("Lead Details")}}</h4>
|
||||
@ -130,16 +77,14 @@
|
||||
<h5 class="text-body-highlight mb-0 text-end">{{ _("Status")}}
|
||||
{% if lead.status == "new" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{_("New")}}</span><span class="fa fa-bell ms-1"></span></span>
|
||||
{% elif lead.status == "follow_up" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{_("Follow Up")}}</span><span class="fa fa-clock-o ms-1"></span></span>
|
||||
{% elif lead.status == "negotiation" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Negotiation")}}</span><span class="fa fa-wrench ms-1"></span></span>
|
||||
{% elif lead.status == "won" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("Won")}}</span><span class="fa fa-check ms-1"></span></span>
|
||||
{% elif lead.status == "lost" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{_("Lost")}}</span><span class="fa fa-times ms-1"></span></span>
|
||||
{% elif lead.status == "closed" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{_("Closed")}}</span><span class="fa fa-times ms-1"></span></span>
|
||||
{% elif lead.status == "contacted" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{_("Contacted")}}</span><span class="fa fa-clock-o ms-1"></span></span>
|
||||
{% elif lead.status == "qualified" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Qualified")}}</span><span class="fa fa-wrench ms-1"></span></span>
|
||||
{% elif lead.status == "unqualified" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("Unqualified")}}</span><span class="fa fa-check ms-1"></span></span>
|
||||
{% elif lead.status == "converted" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{_("Converted")}}</span><span class="fa fa-times ms-1"></span></span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
@ -149,9 +94,10 @@
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3 text-center text-xxl-start">
|
||||
<div class="col-6 col-sm-auto flex-1">
|
||||
<h5 class="fw-bolder mb-2">{{ _("Car Requested") }}</h5>
|
||||
{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}
|
||||
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
|
||||
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Car Requested") }}</h5>
|
||||
<img src="{{ lead.id_car_make.logo.url }}" alt="Car Make Logo" class="img-fluid rounded mb-2" style="width: 60px; height: 60px;">
|
||||
<p class="mb-0">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -170,12 +116,6 @@
|
||||
</div>
|
||||
<span class="text-body-secondary">{{ lead.phone_number}} </span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center mb-1"><span class="currency">{{CURRENCY}}</span>
|
||||
<h5 class="text-body-highlight fw-bold mb-0">{{ _("Salary")}}</h5>
|
||||
</div>
|
||||
<p class="mb-0 text-body-secondary"><small><span class="currency">{{CURRENCY}}</span></small> {{lead.salary}} </p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-clock"></span>
|
||||
<h5 class="text-body-highlight fw-bold mb-0">{{ _("Created")}}</h5>
|
||||
@ -200,12 +140,6 @@
|
||||
</div>
|
||||
<span class="text-body-secondary">{{ lead.address}}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-map"></span>
|
||||
<h5 class="text-body-highlight fw-bold mb-0">{{ _("City") }}</h5>
|
||||
</div>
|
||||
<span class="text-body-secondary">{{ lead.city }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,12 +152,12 @@
|
||||
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> <small>{% trans "Next Action" %} :</small> <small>{{lead.next_action_date|naturalday|capfirst}}</small></div>
|
||||
</div>
|
||||
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;">
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="opportunity-tab" data-bs-toggle="tab" href="#tab-opportunity" role="tab" aria-controls="tab-opportunity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Opportunities") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
||||
<li class="nav-item text-nowrap ml-auto" role="presentation">
|
||||
<a href="{% url 'opportunity_create' %}" class="btn btn-info btn-sm" type="button"> <i class="fa-solid fa-user-plus me-2"></i> Create Opportunity</a>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
|
||||
<i class="fa-solid fa-user-plus me-2"></i>
|
||||
@ -252,9 +186,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<div class="tab-pane fade" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-4" id="scrollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3>
|
||||
<h3 class="mb-4" id="s crollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3>
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
|
||||
</div>
|
||||
<div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top">
|
||||
@ -300,6 +234,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade active show" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
|
||||
<a href="{% url 'opportunity_create' %}" class="btn btn-phoenix-primary btn-sm" type="button"> <i class="fa-solid fa-plus me-2"></i>{{ _("Add Opportunity") }}</a>
|
||||
</div>
|
||||
|
||||
<div class="border-top border-bottom border-translucent" id="leadDetailsTable">
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Car") }}</th>
|
||||
<th class="align-middle text-start text-uppercase" scope="col" style="width:20%;">{{ _("Probability")}}</th>
|
||||
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:20%;">{{ _("Priority")}}</th>
|
||||
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody >
|
||||
{% for opportunity in lead.get_opportunities %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{opportunity.car}}</td>
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{opportunity.probability}}</td>
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{opportunity.priority|capfirst}}</td>
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1"><a class="btn btn-sm btn-phoenix-primary" href="{% url 'opportunity_detail' opportunity.slug %}">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
|
||||
<div class="mb-1 d-flex align-items-center justify-content-between">
|
||||
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
|
||||
@ -319,7 +285,6 @@
|
||||
</thead>
|
||||
<tbody >
|
||||
{% for note in notes %}
|
||||
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
|
||||
{% if note.created_by.staff %}
|
||||
@ -502,12 +467,12 @@
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="all-email-table-body">
|
||||
<tbody class="list" id="all-tasks-table-body">
|
||||
{% for task in tasks %}
|
||||
<tr class="task-{{task.pk}} hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
|
||||
<td class="fs-9 align-middle px-0 py-3">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-select=".task-{{task.pk}}" hx-target=".task-{{task.pk}}" />
|
||||
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' 'lead' task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target=".task-{{task.pk}}" hx-select=".task-{{task.pk}}" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
|
||||
|
||||
@ -6,60 +6,7 @@
|
||||
<div class="row g-3">
|
||||
<h2 class="mb-4">{{ _("Leads")|capfirst }}</h2>
|
||||
<!-- Action Tracking Modal -->
|
||||
<div class="modal fade" id="actionTrackingModal" tabindex="-1" aria-labelledby="actionTrackingModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="actionTrackingModalLabel">{{ _("Update Lead Actions") }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="actionTrackingForm" method="post">
|
||||
<div class="modal-body">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="leadId" name="lead_id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="currentAction" class="form-label">{{ _("Current Action") }}</label>
|
||||
<select class="form-select" id="currentAction" name="current_action" required>
|
||||
<option value="">{{ _("Select Action") }}</option>
|
||||
<option value="contacted">{{ _("Contacted") }}</option>
|
||||
<option value="follow_up">{{ _("Follow Up") }}</option>
|
||||
<option value="proposal_sent">{{ _("Proposal Sent") }}</option>
|
||||
<option value="negotiation">{{ _("Negotiation") }}</option>
|
||||
<option value="closed">{{ _("Closed") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextAction" class="form-label">{{ _("Next Action") }}</label>
|
||||
<select class="form-select" id="nextAction" name="next_action" required>
|
||||
<option value="">{{ _("Select Next Action") }}</option>
|
||||
<option value="call">{{ _("Call") }}</option>
|
||||
<option value="meeting">{{ _("Meeting") }}</option>
|
||||
<option value="email">{{ _("Email") }}</option>
|
||||
<option value="proposal">{{ _("Send Proposal") }}</option>
|
||||
<option value="follow_up">{{ _("Follow Up") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextActionDate" class="form-label">{{ _("Next Action Date") }}</label>
|
||||
<input type="datetime-local" class="form-control" id="nextActionDate" name="next_action_date" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="actionNotes" class="form-label">{{ _("Notes") }}</label>
|
||||
<textarea class="form-control" id="actionNotes" name="action_notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">{{ _("Close") }}</button>
|
||||
<button type="submit" class="btn btn-success">{{ _("Save Changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "crm/leads/partials/update_action.html" %}
|
||||
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
@ -201,15 +148,15 @@
|
||||
{% if schedule.scheduled_type == "call" %}
|
||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
||||
<span class="badge badge-phoenix badge-phoenix-primary text-primary {% if schedule.schedule_past_date %}badge-phoenix-danger text-danger{% endif %} fw-semibold"><span class="text-primary {% if schedule.schedule_past_date %}text-danger{% endif %}" data-feather="phone"></span>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
||||
{% elif schedule.scheduled_type == "meeting" %}
|
||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
||||
<span class="badge badge-phoenix badge-phoenix-success text-success fw-semibold"><span class="text-success" data-feather="calendar"></span>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
||||
{% elif schedule.scheduled_type == "email" %}
|
||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
||||
<span class="badge badge-phoenix badge-phoenix-warning text-warning fw-semibold"><span class="text-warning" data-feather="email"></span>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
53
templates/crm/leads/partials/update_action.html
Normal file
53
templates/crm/leads/partials/update_action.html
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="modal fade" id="actionTrackingModal" tabindex="-1" aria-labelledby="actionTrackingModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="actionTrackingModalLabel">{{ _("Update Lead Actions") }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="actionTrackingForm" method="post">
|
||||
<div class="modal-body">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="leadId" name="lead_id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="currentAction" class="form-label">{{ _("Current Stage") }}</label>
|
||||
<select class="form-select" id="currentAction" name="current_action" required>
|
||||
<option value="">{{ _("Select Stage") }}</option>
|
||||
<option value="new">{{ _("New") }}</option>
|
||||
<option value="contacted">{{ _("Contacted") }}</option>
|
||||
<option value="qualified">{{ _("Qualified") }}</option>
|
||||
<option value="unqualified">{{ _("Unqualified") }}</option>
|
||||
<option value="converted">{{ _("Converted") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextAction" class="form-label">{{ _("Next Action") }}</label>
|
||||
<select class="form-select" id="nextAction" name="next_action" required>
|
||||
<option value="">{{ _("Select Next Action") }}</option>
|
||||
<option value="no_action">{{ _("No Action") }}</option>
|
||||
<option value="call">{{ _("Call") }}</option>
|
||||
<option value="meeting">{{ _("Meeting") }}</option>
|
||||
<option value="email">{{ _("Email") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nextActionDate" class="form-label">{{ _("Next Action Date") }}</label>
|
||||
<input type="datetime-local" class="form-control" id="nextActionDate" name="next_action_date">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="actionNotes" class="form-label">{{ _("Notes") }}</label>
|
||||
<textarea class="form-control" id="actionNotes" name="action_notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -19,6 +19,7 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{% url 'update_opportunity' opportunity.slug %}">Update Opportunity</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'update_opportunity' opportunity.slug %}">Update Stage</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="">Delete Opportunity</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -32,9 +33,9 @@
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-12 col-sm-auto flex-1">
|
||||
{% if opportunity.car %}
|
||||
<h3 class="fw-bolder mb-2 line-clamp-1">{{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}</h3>
|
||||
<h3 class="fw-bolder mb-2 line-clamp-1"><span class="d-inline-block lh-sm me-1" data-feather="check-circle" style="height:16px;width:16px;"></span> {{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}</h3>
|
||||
{% endif %}
|
||||
<h3 class="fw-bolder mb-2 line-clamp-1">{{ opportunity.customer.customer_name }}</h3>
|
||||
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
{% if opportunity.car.finances %}
|
||||
<h5 class="mb-0 me-4">{{ opportunity.car.finances.total }} <span class="fw-light"><span class="currency">{{ CURRENCY }}</span></span></h5>
|
||||
@ -48,60 +49,108 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h5>{{ opportunity.staff.get_local_name}}</h5>
|
||||
<div class="dropdown"><a class="text-body-secondary dropdown-toggle text-decoration-none dropdown-caret-none" href="#!" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Owner<span class="fa-solid fa-caret-down text-body-secondary fs-9 ms-2"></span></a>
|
||||
<div class="dropdown-menu shadow-sm" style="min-width:20rem">
|
||||
<div class="card position-relative border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="mx-3">
|
||||
<div class="text-end">
|
||||
<button class="btn btn-link text-danger" type="button">{{ _("Cancel") }}</button>
|
||||
<button class="btn btn-sm btn-primary px-5" type="button">{{ _("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if opportunity.customer %}
|
||||
<h5>{{ opportunity.customer|capfirst}}</h5>
|
||||
<div class=""><div class="text-body-secondary text-decoration-none">Individual<span class="fa-solid text-body-secondary fs-9 ms-2"></span></div>
|
||||
{% else %}
|
||||
<h5>{{ opportunity.organization|capfirst}}</h5>
|
||||
<div class=""><div class="text-body-secondary text-decoration-none">Organization<span class="fa-solid text-body-secondary fs-9 ms-2"></span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div><span class="badge badge-phoenix badge-phoenix-success me-2">{{ opportunity.get_stage_display }}</span><span class="badge badge-phoenix badge-phoenix-danger me-2">{{ opportunity.get_status_display }}</span></div>
|
||||
<div><span class="badge badge-phoenix badge-phoenix-primary">STAGE</span> : <span class="badge badge-phoenix badge-phoenix-success me-2">{{ opportunity.get_stage_display }}</span><span class="badge badge-phoenix badge-phoenix-danger me-2">{{ opportunity.get_status_display }}</span></div>
|
||||
</div>
|
||||
<div class="progress mb-2" style="height:5px">
|
||||
<div class="progress-bar bg-primary-lighter" data-bs-theme="light" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<p class="mb-0"> {{ opportunity.get_status_display }}</p>
|
||||
<div><span class="d-inline-block lh-sm me-1" data-feather="clock" style="height:16px;width:16px;"></span><span class="d-inline-block lh-sm"> {{ opportunity.created}}</span></div>
|
||||
<div><span class="d-inline-block lh-sm me-1" data-feather="clock" style="height:16px;width:16px;"></span><span class="d-inline-block lh-sm"> {{ opportunity.created|naturaltime|capfirst}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} <div class="card">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-5">{{ _("Other Information")}}</h4>
|
||||
<h4 class="mb-5 d-flex align-items-center"><span class="d-inline-block lh-sm me-1" data-feather="link" style="height:16px;width:16px;"></span> {{ _("Upcoming Events")}}</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Status") }}</h5>
|
||||
<a href="#" class="fw-bold fs-9" hx-on:click="htmx.find('#id_status').disabled = !htmx.find('#id_status').disabled;this.text = htmx.find('#id_status').disabled ? 'Update Status' : 'Cancel'">{{ _("Update Status")}}</a>
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Estimate") }}</h5>
|
||||
</div>
|
||||
{{status_form.status}}
|
||||
{% if opportunity.estimate %}
|
||||
<a class="dropdown-item" href="{% url 'estimate_detail' opportunity.estimate.pk %}">{{ _("View Quotation")}}</a>
|
||||
{% else %}
|
||||
<p>{{ _("No Estimate") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Stage") }}</h5>
|
||||
<a href="#" class="fw-bold fs-9" hx-on:click="htmx.find('#id_stage').disabled = !htmx.find('#id_stage').disabled;this.text = htmx.find('#id_stage').disabled ? 'Update Stage' : 'Cancel'">{{ _("Update Stage")}}</a>
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Invoice") }}</h5>
|
||||
</div>
|
||||
{{status_form.stage}}
|
||||
{% if opportunity.estimate.invoice %}
|
||||
<a class="dropdown-item" href="{% url 'invoice_detail' opportunity.estimate.invoice.pk %}">{{ _("View Invoice")}}</a>
|
||||
{% else %}
|
||||
<p>{{ _("No Invoice") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-5 d-flex align-items-center"><span class="d-inline-block lh-sm me-1" data-feather="link" style="height:16px;width:16px;"></span> {{ _("Related Records")}}</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Estimate") }}</h5>
|
||||
</div>
|
||||
{% if opportunity.estimate %}
|
||||
<a class="dropdown-item" href="{% url 'estimate_detail' opportunity.estimate.pk %}">{{ _("View Quotation")}}</a>
|
||||
{% else %}
|
||||
<p>{{ _("No Estimate") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Invoice") }}</h5>
|
||||
</div>
|
||||
{% if opportunity.estimate.invoice %}
|
||||
<a class="dropdown-item" href="{% url 'invoice_detail' opportunity.estimate.invoice.pk %}">{{ _("View Invoice")}}</a>
|
||||
{% else %}
|
||||
<p>{{ _("No Invoice") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-5 d-flex align-items-center"><span class="d-inline-block lh-sm me-1" data-feather="clock" style="height:16px;width:16px;"></span> {{ _("System Information")}}</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Created ") }}</h5>
|
||||
</div>
|
||||
{{ opportunity.created|naturalday|capfirst }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<h5 class="mb-0 text-body-highlight me-2">{{ _("Last Updated") }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{ opportunity.updated }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-7 col-xxl-8">
|
||||
@ -110,7 +159,7 @@
|
||||
<div class="row g-4 g-xl-1 g-xxl-3 justify-content-between">
|
||||
<div class="col-sm-auto">
|
||||
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center">
|
||||
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-success-dark" data-feather="dollar-sign" style="width:24px; height:24px"></span></div>
|
||||
<div class="d-flex bg-primary-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-primary-dark" data-feather="layout" style="width:24px; height:24px"></span></div>
|
||||
<div>
|
||||
<p class="fw-bold mb-1">{{ _("Quotation Amount") }}</p>
|
||||
<h4 class="fw-bolder text-nowrap">
|
||||
@ -123,19 +172,19 @@
|
||||
</div>
|
||||
<div class="col-sm-auto">
|
||||
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center border-start-sm ps-sm-5 border-translucent">
|
||||
<div class="d-flex bg-info-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-info-dark" data-feather="code" style="width:24px; height:24px"></span></div>
|
||||
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-success-dark currency" style="width:24px; height:24px">{{CURRENCY}}</span></div>
|
||||
<div>
|
||||
<p class="fw-bold mb-1">Code</p>
|
||||
<h4 class="fw-bolder text-nowrap">PHO1234</h4>
|
||||
<p class="fw-bold mb-1">{{ _("Amount") }}</p>
|
||||
<h4 class="fw-bolder text-nowrap">{{opportunity.amount}}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-auto">
|
||||
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center border-start-sm ps-sm-5 border-translucent">
|
||||
<div class="d-flex bg-primary-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-primary-dark" data-feather="layout" style="width:24px; height:24px"></span></div>
|
||||
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-success-dark currency" style="width:24px; height:24px">{{CURRENCY}}</span></div>
|
||||
<div>
|
||||
<p class="fw-bold mb-1">Type</p>
|
||||
<h4 class="fw-bolder text-nowrap">New Business</h4>
|
||||
<p class="fw-bold mb-1">{{ _("Expected Revenue") }}</p>
|
||||
<h4 class="fw-bolder text-nowrap">{{opportunity.expected_revenue}}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,13 +209,13 @@
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ opportunity.probability }}</p>
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ opportunity.probability }} (%)</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-info-subtle rounded-circle flex-center me-3" style="width:24px; height:24px"><span class="text-info-dark" data-feather="trending-up" style="width:16px; height:16px"></span></div>
|
||||
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-info-dark currency" style="width:24px; height:24px">{{CURRENCY}}</span></div>
|
||||
<p class="fw-bold mb-0">{{ _("Estimated Revenue") }}</p>
|
||||
</div>
|
||||
</td>
|
||||
@ -192,7 +241,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0 text-body" href="tel:{{ opportunity.customer.phone_number }}">{{ opportunity.customer.phone }}</a></td>
|
||||
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0 text-body" href="tel:{{ opportunity.customer.phone_number }}">{{ opportunity.customer.phone_number }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
@ -222,19 +271,27 @@
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0">{{ opportunity.customer.get_full_name}}</div>
|
||||
{% if opportunity.customer %}
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0">{{ opportunity.customer.full_name}}</div>
|
||||
{% else %}
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0">{{ opportunity.organization}}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-info-subtle rounded-circle flex-center me-3" style="width:24px; height:24px"><span class="text-info-dark" data-feather="edit" style="width:16px; height:16px"></span></div>
|
||||
<p class="fw-bold mb-0">{{ _("Staff") }}</p>
|
||||
<p class="fw-bold mb-0">{{ _("Assigned To") }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.staff.get_local_name}}</div>
|
||||
{% if request.user.email == opportunity.staff.email %}
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0">You</div>
|
||||
{% else %}
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.staff.get_local_name}}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -255,19 +312,19 @@
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0">{{ opportunity.created|date}}</div>
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0">{{ opportunity.created|naturaltime|capfirst}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-warning-subtle rounded-circle flex-center me-3" style="width:24px; height:24px"><span class="text-warning-dark" data-feather="clock" style="width:16px; height:16px"></span></div>
|
||||
<p class="fw-bold mb-0">{{ _("Closing Date")}}</p>
|
||||
<p class="fw-bold mb-0">{{ _("Expected Closing Date")}}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.closing_date|date}}</div>
|
||||
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.expected_close_date|date}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -275,12 +332,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6" id="myTab" role="tablist" style="overflow-y: hidden;">
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>Activity</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>Notes</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>Meeting</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>{{ _("Activity") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>{{ _("Notes") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>{{ _("Meetings") }}</a></li>
|
||||
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="task-tab" data-bs-toggle="tab" href="#tab-task" role="tab" aria-controls="tab-task" aria-selected="true"> <span class="fa-solid fa-square-check me-2 tab-icon-color"></span>Task</a></li> {% endcomment %}
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>Call</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>Emails </a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>{{ _("Calls") }}</a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>{{ _("Emails")}} </a></li>
|
||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
||||
|
||||
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="attachments-tab" data-bs-toggle="tab" href="#tab-attachments" role="tab" aria-controls="tab-attachments" aria-selected="true"> <span class="fa-solid fa-paperclip me-2 tab-icon-color"></span>Attachments</a></li> {% endcomment %}
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
@ -300,6 +359,43 @@
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% for activity in lead_activities %}
|
||||
<div class="border-bottom border-translucent py-4">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" style="width:25px; height:25px">
|
||||
{% if activity.activity_type == "call" %}
|
||||
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
||||
{% elif activity.activity_type == "email" %}
|
||||
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
||||
{% elif activity.activity_type == "meeting" %}
|
||||
<span class="fa-solid fa-users text-danger fs-8"></span>
|
||||
{% elif activity.activity_type == "whatsapp" %}
|
||||
<span class="fab fa-whatsapp text-success-dark fs-7"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm"></h5>
|
||||
<p class="fs-9 mb-0">{{activity.notes}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm"></h5>
|
||||
{% if request.user.email == activity.created_by %}
|
||||
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
|
||||
{% else %}
|
||||
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
|
||||
</div>
|
||||
<p class="fs-9 mb-0"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for activity in activities %}
|
||||
<div class="border-bottom border-translucent py-4">
|
||||
<div class="d-flex">
|
||||
@ -318,7 +414,17 @@
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm"></h5>
|
||||
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
|
||||
<p class="fs-9 mb-0">{{activity.notes}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm"></h5>
|
||||
{% if request.user.email == activity.created_by %}
|
||||
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
|
||||
{% else %}
|
||||
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
|
||||
</div>
|
||||
@ -337,11 +443,20 @@
|
||||
</form>
|
||||
<div class="row gy-4 note-list">
|
||||
<div class="col-12 col-xl-auto flex-1">
|
||||
{% for note in lead_notes %}
|
||||
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
|
||||
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
|
||||
<div class="d-flex">
|
||||
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
|
||||
<p class="fs-9 mb-0 text-body-tertiary text-opacity-85">by<a class="ms-1 fw-semibold" href="#!">{{note.created_by}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for note in notes %}
|
||||
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
|
||||
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
|
||||
<div class="d-flex">
|
||||
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created}}</span></div>
|
||||
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
|
||||
<p class="fs-9 mb-0 text-body-tertiary text-opacity-85">by<a class="ms-1 fw-semibold" href="#!">{{note.created_by}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@ -412,7 +527,7 @@
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="description align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2 pe-6">{{call.purpose}}</td>
|
||||
<td class="create_date text-end align-middle white-space-nowrap text-body py-2">{{call.scheduled_by}}</td>
|
||||
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{call.created_at}}</td>
|
||||
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{call.created_at|naturaltime|capfirst}}</td>
|
||||
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
@ -507,6 +622,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" data-bulk-select='{"body":"all-email-table-body"}' />
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
|
||||
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="all-tasks-table-body">
|
||||
|
||||
{% for task in tasks %}
|
||||
{% include "partials/task.html" with task=task content_type="opportunity" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
|
||||
<div class="col-auto d-flex">
|
||||
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body" data-list-info="data-list-info"></p><a class="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
|
||||
</div>
|
||||
<div class="col-auto d-flex">
|
||||
<button class="page-link" data-list-pagination="prev"><span class="fas fa-chevron-left"></span></button>
|
||||
<ul class="mb-0 pagination"></ul>
|
||||
<button class="page-link pe-0" data-list-pagination="next"><span class="fas fa-chevron-right"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-attachments" role="tabpanel" aria-labelledby="attachments-tab">
|
||||
<h2 class="mb-3">Attachments</h2>
|
||||
<div class="border-top border-dashed pt-3 pb-4">
|
||||
@ -554,4 +714,5 @@
|
||||
</div>
|
||||
</div>
|
||||
{% include "components/activity_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||
{% endblock %}
|
||||
@ -81,6 +81,22 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Amount Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.amount.id_for_label }}">
|
||||
{{ form.amount.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span class="currency">{{CURRENCY}}</span></span>
|
||||
{{ form.amount|add_class:"form-control" }}
|
||||
</div>
|
||||
{% if form.amount.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.amount.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Probability Field -->
|
||||
<div class="mb-4">
|
||||
@ -107,7 +123,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Expected Revenue -->
|
||||
<!-- Expected Revenue -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">
|
||||
{{ form.expected_revenue.label }}
|
||||
@ -123,18 +139,19 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Closing Date -->
|
||||
<div class="mb-5">
|
||||
<label class="form-label" for="{{ form.closing_date.id_for_label }}">
|
||||
{{ form.closing_date.label }}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
{{ form.closing_date|add_class:"form-control" }}
|
||||
{{ form.expected_close_date|add_class:"form-control" }}
|
||||
<span class="input-group-text"><span class="far fa-calendar"></span></span>
|
||||
</div>
|
||||
{% if form.closing_date.errors %}
|
||||
{% if form.expected_close_date.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.closing_date.errors }}
|
||||
{{ form.expected_close_date.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -188,6 +205,10 @@
|
||||
|
||||
<script>
|
||||
function updateProbabilityValue(value) {
|
||||
const amount = document.getElementById('id_amount');
|
||||
const expectedRevenue = document.getElementById('id_expected_revenue');
|
||||
expectedRevenue.value = (parseFloat(amount.value) * value / 100).toFixed(2);
|
||||
|
||||
const badge = document.getElementById('probability-value');
|
||||
badge.textContent = value + '%';
|
||||
|
||||
|
||||
@ -19,21 +19,37 @@
|
||||
{% if opportunity.get_stage_display == 'Closed Won' %}bg-success-soft
|
||||
{% elif opportunity.get_stage_display == 'Closed Lost' %}bg-danger-soft{% endif %}">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-4">Opportunity for {{ opportunity.customer.customer_name }}</h5>
|
||||
<div class="avatar avatar-xl me-3 mb-3">
|
||||
{% if opportunity.car.id_car_make.logo %}
|
||||
<img class="rounded" src="{{ opportunity.car.id_car_make.logo.url }}" alt="" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if opportunity.customer %}
|
||||
<h5 class="mb-4">Opportunity for {{ opportunity.customer }}</h5>
|
||||
{% elif opportunity.organization %}
|
||||
<h5 class="mb-4">Opportunity for {{ opportunity.organization }}</h5>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="d-flex gap-2">
|
||||
{% if opportunity.get_stage_display == "Negotiation" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-primary">{{ opportunity.get_stage_display }}</span>
|
||||
{% elif opportunity.get_stage_display == "Discovery" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-info">{{ opportunity.get_stage_display }}</span>
|
||||
{% elif opportunity.get_stage_display == "Proposal" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning">{{ opportunity.get_stage_display }}</span>
|
||||
{% elif opportunity.get_stage_display == "Closed Won" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">{{ opportunity.get_stage_display }}</span>
|
||||
{% elif opportunity.get_stage_display == "Closed Lost" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-danger">{{ opportunity.get_stage_display }}</span>
|
||||
{% if opportunity.stage == "qualification" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-primary">
|
||||
{% elif opportunity.stage == "test_drive" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-info">
|
||||
{% elif opportunity.stage == "quotation" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning">
|
||||
{% elif opportunity.stage == "negotiation" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning">
|
||||
{% elif opportunity.stage == "financing" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning">
|
||||
{% elif opportunity.stage == "closed_won" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
|
||||
{% elif opportunity.stage == "closed_lost" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-danger">
|
||||
{% elif opportunity.stage == "on_hold" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary">
|
||||
{% endif %}
|
||||
{{ opportunity.stage }}</span>
|
||||
<span class="badge badge-phoenix fs-10
|
||||
{% if opportunity.get_stage_display == 'Won' %}badge-phoenix-success
|
||||
{% elif opportunity.get_stage_display == 'Lost' %}badge-phoenix-danger{% endif %}">
|
||||
@ -49,7 +65,12 @@
|
||||
<div class="deals-company-agent d-flex justify-content-between mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="uil uil-user me-2"></span>
|
||||
<p class="text-body-secondary fw-bold fs-10 mb-0">{{ opportunity.staff.name }}</p>
|
||||
<p class="text-body-secondary fw-bold fs-10 mb-0">
|
||||
{{ _("Assigned To") }}{% if request.user.email == opportunity.staff.email %}
|
||||
{{ _("You") }}
|
||||
{% else %}
|
||||
{{ opportunity.staff.name }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="mb-3 w-100">
|
||||
@ -73,7 +94,9 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<p class="fw-semibold fs-9 mb-0 text-body-emphasis">{{ opportunity.closing_date|naturalday|capfirst }}</p>
|
||||
{% if opportunity.expected_close_date %}
|
||||
<p class="fw-semibold fs-9 mb-0 text-body-emphasis">{{ opportunity.expected_close_date|naturalday|capfirst }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
20
templates/partials/task.html
Normal file
20
templates/partials/task.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% load static i18n humanize %}
|
||||
<tr id="task-{{task.pk}}" class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
|
||||
<td class="fs-9 align-middle px-0 py-3">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' content_type=content_type pk=task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target="#task-{{task.pk}}" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
|
||||
<div class="fs-10 d-block">{{task.description}}</div>
|
||||
</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.assigned_to}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created|naturalday|capfirst}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">
|
||||
{% if task.completed %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning"><i class="fa-solid fa-xmark"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
283
templates/sales/estimates/sale_order_form1.html
Normal file
283
templates/sales/estimates/sale_order_form1.html
Normal file
@ -0,0 +1,283 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% block title %}
|
||||
{% trans 'Sale Order' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* Custom styling */
|
||||
.form-section {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.required-field::after {
|
||||
content: " *";
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-select input {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.search-select .search-icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.currency-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.currency-input .currency-symbol {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.currency-input input {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
position: sticky;
|
||||
bottom: 1rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0">
|
||||
<i class="fas fa-file-invoice me-2"></i> New Sale Order
|
||||
</h2>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form id="saleOrderForm" method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Basic Information Section -->
|
||||
<div class="form-section">
|
||||
<h4 class="form-section-header">
|
||||
<i class="fas fa-info-circle me-2"></i> Basic Information
|
||||
</h4>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Estimate -->
|
||||
<div class="col-md-6">
|
||||
{{form.estimate|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
<!-- Opportunity -->
|
||||
<div class="col-md-6">
|
||||
{{form.opportunity|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
<!-- Customer -->
|
||||
<div class="col-md-6">
|
||||
{% if form.customer %}
|
||||
{{form.customer}}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Vehicle -->
|
||||
<div class="col-md-6">
|
||||
<label for="car" class="form-label required-field">Vehicles</label>
|
||||
<ul class="list-group">
|
||||
{% for car in data.cars %}
|
||||
<li class="list-group-item d-flex justify-content-around align-items-center">
|
||||
<span class="badge bg-info rounded-pill">{{ car.make }}</span>
|
||||
<span class="badge bg-info rounded-pill">{{ car.model }}</span>
|
||||
<span class="badge bg-info rounded-pill">{{ car.year }}</span>
|
||||
<span class="badge bg-info rounded-pill">{{ car.vin }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Payment Method -->
|
||||
<div class="col-md-6">
|
||||
{{form.payment_method|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="col-md-6">
|
||||
{{form.status|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financial Details Section -->
|
||||
<div class="form-section">
|
||||
<h4 class="form-section-header">
|
||||
<i class="fas fa-money-bill-wave me-2"></i> Financial Details
|
||||
</h4>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Agreed Price -->
|
||||
<div class="col-md-6">
|
||||
{{form.agreed_price|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
<!-- Down Payment Amount -->
|
||||
<div class="col-md-6">
|
||||
<div class="currency-input">
|
||||
{{form.down_payment_amount|as_crispy_field}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Loan Amount -->
|
||||
<div class="col-md-6">
|
||||
{{form.loan_amount|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery Information Section -->
|
||||
<div class="form-section">
|
||||
<h4 class="form-section-header">
|
||||
<i class="fas fa-truck me-2"></i> Delivery Information
|
||||
</h4>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Expected Delivery Date -->
|
||||
<div class="col-md-6">
|
||||
{{form.expected_delivery_date|as_crispy_field}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="col-12">
|
||||
<label for="comments" class="form-label">Comments</label>
|
||||
<textarea class="form-control" id="comments" rows="3" placeholder="Enter any additional comments..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions mt-4">
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'estimate_detail' estimate.pk %}" type="button" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-2"></i> Cancel
|
||||
</a>
|
||||
<div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-check-circle me-2"></i> Submit Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<!-- Custom JavaScript -->
|
||||
<script>
|
||||
/*
|
||||
// Calculate financial totals
|
||||
function calculateTotals() {
|
||||
const agreedPrice = parseFloat(document.getElementById('agreed_price').value) || 0;
|
||||
const downPayment = parseFloat(document.getElementById('down_payment_amount').value) || 0;
|
||||
const tradeInValue = parseFloat(document.getElementById('trade_in_value').value) || 0;
|
||||
const loanAmount = parseFloat(document.getElementById('loan_amount').value) || 0;
|
||||
|
||||
// Calculate total paid amount
|
||||
const totalPaid = downPayment + tradeInValue + loanAmount;
|
||||
document.getElementById('total_paid_amount').value = totalPaid.toFixed(2);
|
||||
|
||||
// Calculate remaining balance
|
||||
const remainingBalance = agreedPrice - totalPaid;
|
||||
document.getElementById('remaining_balance').value = remainingBalance > 0 ? remainingBalance.toFixed(2) : '0.00';
|
||||
}
|
||||
|
||||
// Show/hide cancellation fields based on status
|
||||
function toggleCancellationFields() {
|
||||
const status = document.getElementById('status').value;
|
||||
const cancellationFields = document.getElementById('cancellationFields');
|
||||
const cancellationReasonFields = document.getElementById('cancellationReasonFields');
|
||||
|
||||
if (status === 'CANCELLED') {
|
||||
cancellationFields.style.display = 'block';
|
||||
cancellationReasonFields.style.display = 'block';
|
||||
} else {
|
||||
cancellationFields.style.display = 'none';
|
||||
cancellationReasonFields.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Set current datetime for order date
|
||||
function setCurrentDateTime() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
document.getElementById('order_date').value = `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// Initialize form
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set current datetime
|
||||
setCurrentDateTime();
|
||||
|
||||
// Add event listeners for financial calculations
|
||||
document.getElementById('agreed_price').addEventListener('change', calculateTotals);
|
||||
document.getElementById('down_payment_amount').addEventListener('change', calculateTotals);
|
||||
document.getElementById('trade_in_value').addEventListener('change', calculateTotals);
|
||||
document.getElementById('loan_amount').addEventListener('change', calculateTotals);
|
||||
|
||||
// Add event listener for status change
|
||||
document.getElementById('status').addEventListener('change', toggleCancellationFields);
|
||||
|
||||
// Form submission
|
||||
document.getElementById('saleOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
// In a real application, this would submit the form data to the server
|
||||
alert('Sale order would be submitted here');
|
||||
// window.location.href = '/sale-orders/';
|
||||
});
|
||||
});*/
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
619
templates/sales/orders/order_details.html
Normal file
619
templates/sales/orders/order_details.html
Normal file
@ -0,0 +1,619 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n humanize %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* Custom CSS for additional styling */
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1.5rem;
|
||||
top: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-upload:hover {
|
||||
border-color: #0d6efd;
|
||||
background-color: rgba(13, 110, 253, 0.05);
|
||||
}
|
||||
|
||||
.document-thumbnail {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-0">
|
||||
<!-- Header -->
|
||||
<header class="bg-primary text-white py-3">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1 class="h4 mb-0">
|
||||
<i class="fas fa-file-invoice me-2"></i>
|
||||
Sale Order #{{ saleorder.formatted_order_id }}
|
||||
</h1>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-light me-2">
|
||||
<i class="fas fa-print me-1"></i> Print
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-light">
|
||||
<i class="fas fa-share-alt me-1"></i> Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<!-- Left Column -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<!-- Order Summary Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-light">
|
||||
<h5 class="mb-0 text-primary">Order Summary</h5>
|
||||
<span class="status-badge
|
||||
{% if saleorder.status == 'approved' %}bg-success text-white
|
||||
{% elif saleorder.status == 'cancelled' %}bg-danger text-white
|
||||
{% elif saleorder.status == 'pending_approval' %}bg-warning text-dark
|
||||
{% elif saleorder.status == 'delivered' %}bg-info text-white
|
||||
{% else %}bg-secondary text-white{% endif %}">
|
||||
{{ saleorder.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Order Date</label>
|
||||
<p class="mb-0 fw-bold">{{ saleorder.order_date|date }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Customer</label>
|
||||
<p class="mb-0 fw-bold">{{ saleorder.customer.full_name|capfirst }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Payment Method</label>
|
||||
<p class="mb-0 fw-bold">{{ saleorder.get_payment_method_display }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Created By</label>
|
||||
<p class="mb-0 fw-bold">{{ saleorder.created_by }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Expected Delivery</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
{% if saleorder.expected_delivery_date %}
|
||||
{{ saleorder.expected_delivery_date|date }}
|
||||
{% else %}
|
||||
<span class="text-warning">Not scheduled</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Last Updated</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
{{ saleorder.updated_at|naturaltime|capfirst }} by
|
||||
{{ saleorder.last_modified_by }}
|
||||
</p>
|
||||
</div>
|
||||
{% if saleorder.status == 'cancelled' %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Cancellation Reason</label>
|
||||
<p class="mb-0 fw-bold text-danger">{{ saleorder.cancellation_reason|default:"Not specified" }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if saleorder.comments %}
|
||||
<div class="mt-3">
|
||||
<label class="form-label text-muted small mb-1">Order Comments</label>
|
||||
<blockquote class="blockquote mb-0">
|
||||
<p class="mb-0">{{ saleorder.comments }}</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicle Details Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Vehicle Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% if data.cars %}
|
||||
{% for car in data.cars %}
|
||||
<div class="col-md-4 mb-3">
|
||||
<img src="{{ car.logo|default:'https://via.placeholder.com/300x200?text=Vehicle+Image' }}"
|
||||
alt="Vehicle" class="img-fluid rounded" width="200" height="100">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">Make</label>
|
||||
<p class="mb-0">{{ car.make }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">Model</label>
|
||||
<p class="mb-0">{{ car.model }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">Year</label>
|
||||
<p class="mb-0">{{ car.year }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">VIN</label>
|
||||
<p class="mb-0">{{ car.vin }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label text-muted small mb-1">Mileage</label>
|
||||
<p class="mb-0">{{ car.mileage|intcomma }} km</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="col-12 text-center py-4">
|
||||
<p class="text-muted">No vehicle assigned to this order</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financial Details Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Financial Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Agreed Price</label>
|
||||
<p class="mb-0 fw-bold">SAR {{ saleorder.agreed_price|intcomma }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Down Payment</label>
|
||||
<p class="mb-0">SAR {{ saleorder.down_payment_amount|intcomma }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Trade-In Value</label>
|
||||
<p class="mb-0">SAR {{ saleorder.trade_in_value|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Loan Amount</label>
|
||||
<p class="mb-0">SAR {{ saleorder.loan_amount|intcomma }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Total Paid</label>
|
||||
<p class="mb-0">SAR {{ saleorder.total_paid_amount|intcomma }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Remaining Balance</label>
|
||||
<p class="mb-0 fw-bold {% if saleorder.remaining_balance > 0 %}text-danger{% else %}text-success{% endif %}">
|
||||
SAR {{ saleorder.remaining_balance|intcomma }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-3" style="height: 10px;">
|
||||
{% widthratio saleorder.total_paid_amount saleorder.agreed_price 100 as payment_percentage %}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ payment_percentage }}%;"
|
||||
aria-valuenow="{{ payment_percentage }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1 small text-muted">
|
||||
<span>{{ payment_percentage }}% Paid</span>
|
||||
<span>SAR {{ saleorder.agreed_price|intcomma }} Total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Documents</h5>
|
||||
<button class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-plus me-1"></i> Add Document
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="file-upload mb-3">
|
||||
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-2"></i>
|
||||
<p class="mb-1">Drag & drop files here or click to browse</p>
|
||||
<p class="small text-muted mb-0">PDF, JPG, PNG up to 10MB</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% for document in saleorder.documents.all %}
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card">
|
||||
{% if document.file.url|lower|slice:'-3:' == 'pdf' %}
|
||||
<img src="{% static 'images/pdf-icon.png' %}" class="document-thumbnail card-img-top" alt="PDF Document">
|
||||
{% else %}
|
||||
<img src="{{ document.file.url }}" class="document-thumbnail card-img-top" alt="Document">
|
||||
{% endif %}
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text small mb-1">{{ document.get_filename }}</p>
|
||||
<p class="card-text small text-muted mb-0">Added: {{ document.created_at|date:"F j, Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-3">
|
||||
<p class="text-muted">No documents uploaded yet</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments Card -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Comments & Notes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% comment %} <form method="post" action="{% url 'add_sale_order_comment' saleorder.pk %}"> {% endcomment %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" name="comment" rows="3" placeholder="Add a comment or note..." required></textarea>
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="timeline">
|
||||
{% for comment in saleorder.comments.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="card mb-2">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<strong>{{ comment.created_by.get_full_name|default:comment.created_by.username }}</strong>
|
||||
<small class="text-muted">{{ comment.created_at|date:"F j, Y H:i A" }}</small>
|
||||
</div>
|
||||
<p class="mb-0">{{ comment.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<p class="text-muted">No comments yet</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Actions Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Order Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if saleorder.status == 'pending_approval' %}
|
||||
<button class="btn btn-success" onclick="updateStatus('approved')">
|
||||
<i class="fas fa-check-circle me-2"></i> Approve Order
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% comment %} <a href="{% url 'edit_sale_order' saleorder.pk %}" class="btn btn-primary"> {% endcomment %}
|
||||
<a href="" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-2"></i> Edit Order
|
||||
</a>
|
||||
|
||||
{% if not saleorder.invoice %}
|
||||
{% comment %} <a href="{% url 'create_invoice_from_order' saleorder.pk %}" class="btn btn-info"> {% endcomment %}
|
||||
<a href="" class="btn btn-info">
|
||||
<i class="fas fa-file-invoice-dollar me-2"></i> Create Invoice
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if saleorder.status == 'approved' and not saleorder.actual_delivery_date %}
|
||||
<button class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#deliveryModal">
|
||||
<i class="fas fa-truck me-2"></i> Schedule Delivery
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if saleorder.status != 'cancelled' %}
|
||||
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
|
||||
<i class="fas fa-times-circle me-2"></i> Cancel Order
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Timeline Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Order Status Timeline</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="timeline">
|
||||
{% for log in saleorder.status_logs.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong>{{ log.get_status_display }}</strong>
|
||||
<small class="text-muted">{{ log.created_at|date:"F j, Y" }}</small>
|
||||
</div>
|
||||
<p class="small mb-0">
|
||||
{% if log.note %}{{ log.note }}{% endif %}
|
||||
<br>
|
||||
<small class="text-muted">Changed by: {{ log.changed_by.get_full_name|default:log.changed_by.username }}</small>
|
||||
</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<p class="text-muted">No status history available</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Items Card -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Related Items</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Estimate</label>
|
||||
<a href="{% url 'estimate_detail' saleorder.estimate.pk %}" target="_blank" rel="noopener noreferrer">
|
||||
<p class="mb-0">
|
||||
<span class="badge bg-success ms-1">{{ saleorder.estimate.estimate_number }} <i class="fas fa-external-link-alt ms-2" style="font-size: 0.8rem;"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Invoice</label>
|
||||
<p class="mb-0">
|
||||
{% if saleorder.invoice %}
|
||||
<a href="{% url 'invoice_detail' saleorder.invoice.pk %}" target="_blank" rel="noopener noreferrer">
|
||||
<p class="mb-0">
|
||||
<span class="badge bg-success ms-1">{{ saleorder.invoice.invoice_number }} <i class="fas fa-external-link-alt ms-2" style="font-size: 0.8rem;"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Not created yet</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Opportunity</label>
|
||||
<a href="{% url 'opportunity_detail' saleorder.opportunity.slug %}" target="_blank" rel="noopener noreferrer">
|
||||
<p class="mb-0">
|
||||
<span class="badge bg-success ms-1">{{ saleorder.opportunity }} <i class="fas fa-external-link-alt ms-2" style="font-size: 0.8rem;"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1">Customer</label>
|
||||
<a href="{% url 'customer_detail' saleorder.customer.slug %}" target="_blank" rel="noopener noreferrer">
|
||||
<p class="mb-0">
|
||||
<span class="badge bg-success ms-1">{{ saleorder.customer.full_name|capfirst }} <i class="fas fa-external-link-alt ms-2" style="font-size: 0.8rem;"></i></span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade-In Vehicle Card -->
|
||||
{% if saleorder.trade_in_vehicle %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Trade-In Vehicle</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-3">
|
||||
<img src="{{ saleorder.trade_in_vehicle.image.url|default:'https://via.placeholder.com/300x200?text=Trade-In' }}"
|
||||
alt="Trade-In Vehicle" class="img-fluid rounded mb-2">
|
||||
<h6 class="mb-1">
|
||||
{{ saleorder.trade_in_vehicle.year }}
|
||||
{{ saleorder.trade_in_vehicle.make }}
|
||||
{{ saleorder.trade_in_vehicle.model }}
|
||||
</h6>
|
||||
<p class="small text-muted mb-2">VIN: {{ saleorder.trade_in_vehicle.vin }}</p>
|
||||
<p class="fw-bold">SAR {{ saleorder.trade_in_value|intcomma }}</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<p class="small mb-1">
|
||||
<i class="fas fa-tachometer-alt me-1 text-muted"></i>
|
||||
{{ saleorder.trade_in_vehicle.mileage|intcomma }} km
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="small mb-1">
|
||||
<i class="fas fa-paint-brush me-1 text-muted"></i>
|
||||
{{ saleorder.trade_in_vehicle.color }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="small mb-1">
|
||||
<i class="fas fa-gas-pump me-1 text-muted"></i>
|
||||
{{ saleorder.trade_in_vehicle.engine }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="small mb-1">
|
||||
<i class="fas fa-cog me-1 text-muted"></i>
|
||||
{{ saleorder.trade_in_vehicle.get_transmission_display }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Order Modal -->
|
||||
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cancelModalLabel">Cancel Order</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
{% comment %} <form method="post" action="{% url 'cancel_sale_order' saleorder.pk %}"> {% endcomment %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="cancellationReason" class="form-label">Reason for Cancellation</label>
|
||||
<textarea class="form-control" id="cancellationReason" name="cancellation_reason" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-danger">Confirm Cancellation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Delivery Modal -->
|
||||
<div class="modal fade" id="deliveryModal" tabindex="-1" aria-labelledby="deliveryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deliveryModalLabel">Schedule Delivery</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
{% comment %} <form method="post" action="{% url 'schedule_delivery' saleorder.pk %}"> {% endcomment %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="deliveryDate" class="form-label">Delivery Date</label>
|
||||
<input type="date" class="form-control" id="deliveryDate" name="delivery_date" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="deliveryNotes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="deliveryNotes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Schedule Delivery</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Status update function
|
||||
function updateStatus(newStatus) {
|
||||
fetch("", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: newStatus
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while updating status');
|
||||
});
|
||||
}
|
||||
|
||||
// Document upload handling
|
||||
document.querySelector('.file-upload').addEventListener('click', function() {
|
||||
// In a real application, this would open a file dialog
|
||||
alert('File upload dialog would open here');
|
||||
});
|
||||
|
||||
// Initialize tooltips
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
{% load i18n static humanize %}
|
||||
|
||||
{% block title %}{{ _("Orders") }}{% endblock title %}
|
||||
|
||||
@ -14,6 +14,9 @@
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Order Number" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Customer" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "For Quotation" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Invoice" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Status" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Expected Delivery" %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col"></th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col"></th>
|
||||
</tr>
|
||||
@ -24,9 +27,21 @@
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ order.formatted_order_id }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ order.estimate.customer.customer_name }}</td>
|
||||
<td class="align-middle product white-space-nowrap">
|
||||
<a href="{% url 'estimate_detail' order.estimate.pk %}">
|
||||
{{ order.estimate }}
|
||||
</a>
|
||||
<a href="{% url 'estimate_detail' order.estimate.pk %}">
|
||||
{{ order.estimate }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle product white-space-nowrap">
|
||||
{% if order.invoice %}
|
||||
<a href="{% url 'invoice_detail' order.invoice.pk %}">
|
||||
{{ order.invoice }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ order.status }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ order.expected_delivery_date|naturalday|capfirst }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">
|
||||
<a class="btn btn-sm btn-success" href="{% url 'sale_order_details' order.estimate.pk order.pk %}">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user