update on the lead and opportunity+more

This commit is contained in:
ismail 2025-06-01 13:19:25 +03:00
parent 638d3854af
commit b8079ebf97
28 changed files with 2941 additions and 642 deletions

View File

@ -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"}),
}

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

View File

@ -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>&nbsp;
<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>&nbsp;{{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> &nbsp; <small>{% trans "Next Action" %} :</small>&nbsp; <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>

View File

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

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

View File

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

View File

@ -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 + '%';

View File

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

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

View 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 %}

View 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 %}

View File

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