update the preview

This commit is contained in:
ismail 2025-05-07 12:20:54 +03:00
parent 9d6d69f1d0
commit b44fb270fa
18 changed files with 631 additions and 471 deletions

View File

@ -10,6 +10,8 @@ from django.core.validators import MinLengthValidator
from django import forms from django import forms
from plans.models import PlanPricing from plans.models import PlanPricing
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from inventory.validators import SaudiPhoneNumberValidator
from .models import CustomGroup, Status, Stage from .models import CustomGroup, Status, Stage
from .mixins import AddClassMixin from .mixins import AddClassMixin
from django_ledger.forms.invoice import ( from django_ledger.forms.invoice import (
@ -46,6 +48,7 @@ from .models import (
CarModel, CarModel,
SaleOrder, SaleOrder,
CarMake, CarMake,
Customer,
DealerSettings DealerSettings
) )
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
@ -58,6 +61,14 @@ import django_tables2 as tables
User = get_user_model() User = get_user_model()
class SaudiPhoneNumberField(forms.CharField):
def __init__(self, *args, **kwargs):
kwargs.setdefault('min_length', 10)
kwargs.setdefault('max_length', 13)
super().__init__(*args, **kwargs)
self.validators.append(SaudiPhoneNumberValidator())
class AdditionalServiceForm(forms.ModelForm): class AdditionalServiceForm(forms.ModelForm):
""" """
A form used for creating and updating instances of the A form used for creating and updating instances of the
@ -104,20 +115,14 @@ class StaffForm(forms.ModelForm):
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
queryset=Service.objects.all(), queryset=Service.objects.all(),
required=False,) required=False,)
phone_number = forms.CharField( phone_number = SaudiPhoneNumberField(
required=False, required=False,
max_length=10,
min_length=10,
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': _('Phone Number'), 'placeholder': _('Phone Number'),
'id': 'phone' 'id': 'phone'
}), }),
label=_('Phone Number'), label=_('Phone Number'),
validators=[RegexValidator(
regex=r'^05[0-9]{8}$',
message=_('Enter a valid phone number (8-15 digits, starting with 05)')
)]
) )
class Meta: class Meta:
model = Staff model = Staff
@ -147,7 +152,9 @@ class DealerForm(forms.ModelForm):
:type address: str :type address: str
:ivar logo: Logo of the dealer. :ivar logo: Logo of the dealer.
:type logo: File :type logo: File
""" """
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
class Meta: class Meta:
model = Dealer model = Dealer
fields = [ fields = [
@ -161,61 +168,78 @@ class DealerForm(forms.ModelForm):
] ]
class CustomerForm(forms.Form): class CustomerForm(forms.ModelForm):
""" phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
Represents a form for collecting customer information.
This form is used to gather and validate customer details such as name, class Meta:
email, phone number, national ID, tax registration details, and address. model = Customer
It includes several fields with validation constraints and specific fields = [
requirements. It is designed to handle both required and optional fields, 'title',
ensuring the correctness of user inputs. "first_name",
"last_name",
"email",
"phone_number",
"national_id",
"dob",
"address",
'image',
]
# class CustomerForm(forms.Form):
# """
# Represents a form for collecting customer information.
:ivar first_name: Customer's first name. # This form is used to gather and validate customer details such as name,
:type first_name: forms.CharField # email, phone number, national ID, tax registration details, and address.
:ivar last_name: Customer's last name. # It includes several fields with validation constraints and specific
:type last_name: forms.CharField # requirements. It is designed to handle both required and optional fields,
:ivar arabic_name: Customer's name in Arabic. # ensuring the correctness of user inputs.
:type arabic_name: forms.CharField
:ivar email: Customer's email address. # :ivar first_name: Customer's first name.
:type email: forms.EmailField # :type first_name: forms.CharField
:ivar phone_number: Customer's phone number. Validates the format and # :ivar last_name: Customer's last name.
ensures the number is in the Saudi Arabia region. # :type last_name: forms.CharField
:type phone_number: PhoneNumberField # :ivar arabic_name: Customer's name in Arabic.
:ivar national_id: Customer's national ID. Optional field limited to # :type arabic_name: forms.CharField
a maximum length of 10 characters. # :ivar email: Customer's email address.
:type national_id: forms.CharField # :type email: forms.EmailField
:ivar crn: Commercial registration number (CRN) of the customer. Optional field. # :ivar phone_number: Customer's phone number. Validates the format and
:type crn: forms.CharField # ensures the number is in the Saudi Arabia region.
:ivar vrn: Value-added tax registration number (VRN) of the customer. # :type phone_number: PhoneNumberField
Optional field. # :ivar national_id: Customer's national ID. Optional field limited to
:type vrn: forms.CharField # a maximum length of 10 characters.
:ivar address: Customer's address. # :type national_id: forms.CharField
:type address: forms.CharField # :ivar crn: Commercial registration number (CRN) of the customer. Optional field.
""" # :type crn: forms.CharField
first_name = forms.CharField() # :ivar vrn: Value-added tax registration number (VRN) of the customer.
last_name = forms.CharField() # Optional field.
arabic_name = forms.CharField() # :type vrn: forms.CharField
email = forms.EmailField() # :ivar address: Customer's address.
# phone_number = PhoneNumberField( # :type address: forms.CharField
# label=_("Phone Number"), # """
# widget=forms.TextInput( # first_name = forms.CharField()
# attrs={ # last_name = forms.CharField()
# "placeholder": _("Phone"), # arabic_name = forms.CharField()
# } # email = forms.EmailField()
# ), # # phone_number = PhoneNumberField(
# region="SA", # # label=_("Phone Number"),
# error_messages={ # # widget=forms.TextInput(
# "required": _("This field is required."), # # attrs={
# "invalid": _("Phone number must be in the format 05xxxxxxxx"), # # "placeholder": _("Phone"),
# }, # # }
# required=True, # # ),
# ) # # region="SA",
phone_number = forms.CharField(label=_("Phone Number"),min_length=10,max_length=10,validators=[RegexValidator(regex='^05[0-9]{8}$')], required=True) # # error_messages={
national_id = forms.CharField(max_length=10,required=False) # # "required": _("This field is required."),
crn = forms.CharField(required=False) # # "invalid": _("Phone number must be in the format 05xxxxxxxx"),
vrn = forms.CharField(required=False) # # },
address = forms.CharField() # # required=True,
# # )
# phone_number = forms.CharField(label=_("Phone Number"),min_length=10,max_length=10,validators=[RegexValidator(regex='^05[0-9]{8}$')], required=True)
# national_id = forms.CharField(max_length=10,required=False)
# crn = forms.CharField(required=False)
# vrn = forms.CharField(required=False)
# address = forms.CharField()
# image = forms.ImageField(required=False)
class OrganizationForm(CustomerForm): class OrganizationForm(CustomerForm):
@ -479,18 +503,7 @@ class VendorForm(forms.ModelForm):
:ivar Meta: Inner class to define metadata for the Vendor form. :ivar Meta: Inner class to define metadata for the Vendor form.
:type Meta: Type[VendorForm.Meta] :type Meta: Type[VendorForm.Meta]
""" """
phone_number = forms.CharField( phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
label=_("Phone Number"),
min_length=10,
max_length=13,
validators=[
RegexValidator(
regex=r'^(\+9665|05)[0-9]{8}$',
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)")
)
],
required=True
)
contact_person = forms.CharField(label=_("Contact Person")) contact_person = forms.CharField(label=_("Contact Person"))
class Meta: class Meta:
@ -577,6 +590,8 @@ class RepresentativeForm(forms.ModelForm):
:ivar Meta.fields: The fields from the model to include in the form. :ivar Meta.fields: The fields from the model to include in the form.
:type Meta.fields: list of str :type Meta.fields: list of str
""" """
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
class Meta: class Meta:
model = Representative model = Representative
fields = [ fields = [
@ -795,21 +810,7 @@ class WizardForm2(forms.Form):
# }, # },
# required=True, # required=True,
# ) # )
phone_number = forms.CharField( phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
required=False,
max_length=10,
min_length=10,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Phone Number'),
'id': 'phone'
}),
label=_('Phone Number'),
validators=[RegexValidator(
regex=r'^05[0-9]{8}$',
message=_('Enter a valid phone number (10 digits, starting with 05)')
)]
)
class WizardForm3(forms.Form): class WizardForm3(forms.Form):
@ -1006,6 +1007,8 @@ class LeadForm(forms.ModelForm):
options are displayed until a car make is selected. options are displayed until a car make is selected.
:type id_car_model: ModelChoiceField :type id_car_model: ModelChoiceField
""" """
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
id_car_make = forms.ModelChoiceField( id_car_make = forms.ModelChoiceField(
label=_("Make"), label=_("Make"),
queryset=CarMake.objects.filter(is_sa_import=True), queryset=CarMake.objects.filter(is_sa_import=True),
@ -1213,71 +1216,75 @@ class SaleOrderForm(forms.ModelForm):
} }
class EstimateModelCreateForm(EstimateModelCreateFormBase): class EstimateModelCreateForm(forms.Form):
""" title = forms.CharField(max_length=255)
Defines the EstimateModelCreateForm class, which is used to create and manage customer = forms.ModelChoiceField(queryset=Customer.objects.none())
forms for EstimateModel. This form handles the rendering and validation
of specific fields, their input widgets, and labels.
The purpose of this class is to provide a structured way to handle # class EstimateModelCreateForm(EstimateModelCreateFormBase):
EstimateModel instances and their related fields, ensuring a user-friendly # """
form interface with proper field configuration. It facilitates fetching # Defines the EstimateModelCreateForm class, which is used to create and manage
related data, such as customer queries, based on user-specific parameters. # forms for EstimateModel. This form handles the rendering and validation
# of specific fields, their input widgets, and labels.
:ivar ENTITY_SLUG: A string that represents the entity context in which the # The purpose of this class is to provide a structured way to handle
form operates. # EstimateModel instances and their related fields, ensuring a user-friendly
:type ENTITY_SLUG: str # form interface with proper field configuration. It facilitates fetching
:ivar USER_MODEL: The user model that provides methods and objects needed # related data, such as customer queries, based on user-specific parameters.
to filter and query customers.
:type USER_MODEL: Any
:ivar fields: A dictionary representing fields included in the form such as
"title", "customer", and "terms".
:type fields: dict
:ivar widgets: A dictionary defining custom input widgets for form fields,
enabling specific attributes like classes and identifiers for styling or
functionality purposes.
:type widgets: dict
:ivar labels: A dictionary specifying the human-readable labels for form fields.
:type labels: dict
"""
class Meta:
model = ledger_models.EstimateModel
fields = ["title","customer", "terms"]
widgets = {
"customer": forms.Select(
attrs={
"id": "djl-customer-estimate-customer-input",
"class": "input",
"label": _("Customer"),
}
),
'terms': forms.Select(attrs={
'id': 'djl-customer-estimate-terms-input',
'class': 'input',
'label': _('Terms'),
}),
'title': forms.TextInput(attrs={
'id': 'djl-customer-job-title-input',
'class': 'input' + ' is-large',
'label': _('Title'),
})
}
labels = {
'title': _('Title'),
'terms': _('Terms'),
"customer": _("Customer"),
}
def __init__(self, *args, entity_slug, user_model, **kwargs): # :ivar ENTITY_SLUG: A string that represents the entity context in which the
super(EstimateModelCreateForm, self).__init__( # form operates.
*args, entity_slug=entity_slug, user_model=user_model, **kwargs # :type ENTITY_SLUG: str
) # :ivar USER_MODEL: The user model that provides methods and objects needed
self.ENTITY_SLUG = entity_slug # to filter and query customers.
self.USER_MODEL = user_model # :type USER_MODEL: Any
self.fields["customer"].queryset = self.get_customer_queryset() # :ivar fields: A dictionary representing fields included in the form such as
# "title", "customer", and "terms".
# :type fields: dict
# :ivar widgets: A dictionary defining custom input widgets for form fields,
# enabling specific attributes like classes and identifiers for styling or
# functionality purposes.
# :type widgets: dict
# :ivar labels: A dictionary specifying the human-readable labels for form fields.
# :type labels: dict
# """
# class Meta:
# model = ledger_models.EstimateModel
# fields = ["title","customer", "terms"]
# widgets = {
# "customer": forms.Select(
# attrs={
# "id": "djl-customer-estimate-customer-input",
# "class": "input",
# "label": _("Customer"),
# }
# ),
# 'terms': forms.Select(attrs={
# 'id': 'djl-customer-estimate-terms-input',
# 'class': 'input',
# 'label': _('Terms'),
# }),
# 'title': forms.TextInput(attrs={
# 'id': 'djl-customer-job-title-input',
# 'class': 'input' + ' is-large',
# 'label': _('Title'),
# })
# }
# labels = {
# 'title': _('Title'),
# 'terms': _('Terms'),
# "customer": _("Customer"),
# }
def get_customer_queryset(self): # def __init__(self, *args, entity_slug, user_model, **kwargs):
return self.USER_MODEL.dealer.entity.get_customers() # super(EstimateModelCreateForm, self).__init__(
# *args, entity_slug=entity_slug, user_model=user_model, **kwargs
# )
# self.ENTITY_SLUG = entity_slug
# self.USER_MODEL = user_model
# self.fields["customer"].queryset = self.get_customer_queryset()
# def get_customer_queryset(self):
# return self.USER_MODEL.dealer.entity.get_customers()
class OpportunityStatusForm(forms.Form): class OpportunityStatusForm(forms.Form):
@ -1589,21 +1596,7 @@ class PaymentPlanForm(forms.Form):
label=_('Email Address') label=_('Email Address')
) )
phone = forms.CharField( phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
required=False,
max_length=10,
min_length=10,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Phone Number'),
'id': 'phone'
}),
label=_('Phone Number'),
validators=[RegexValidator(
regex=r'^05[0-9]{8}$',
message=_('Enter a valid phone number (10 digits, starting with 05)')
)]
)
# Credit Card Fields (not saved to database) # Credit Card Fields (not saved to database)
card_number = CreditCardField( card_number = CreditCardField(

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-06 14:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_alter_car_vendor'),
]
operations = [
migrations.AddField(
model_name='customer',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-05-06 14:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
('inventory', '0004_customer_image'),
]
operations = [
migrations.AddField(
model_name='customer',
name='customer_model',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-05-06 15:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_customer_customer_model'),
]
operations = [
migrations.RemoveField(
model_name='customer',
name='middle_name',
),
migrations.AddField(
model_name='customer',
name='active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-06 16:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_remove_customer_middle_name_customer_active'),
]
operations = [
migrations.AddField(
model_name='customer',
name='customer_type',
field=models.CharField(blank=True, choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=15, null=True, verbose_name='Customer Type'),
),
]

View File

@ -1032,18 +1032,22 @@ class Priority(models.TextChoices):
HIGH = "high", _("High") HIGH = "high", _("High")
class Customer(models.Model): class Customer(models.Model):
dealer = models.ForeignKey( dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="customers" Dealer, on_delete=models.CASCADE, related_name="customers"
) )
customer_model = models.ForeignKey(
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
title = models.CharField( title = models.CharField(
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
) )
first_name = models.CharField(max_length=50, verbose_name=_("First Name")) first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
middle_name = models.CharField( # middle_name = models.CharField(
max_length=50, blank=True, null=True, verbose_name=_("Middle Name") # max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
) # )
last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
gender = models.CharField( gender = models.CharField(
choices=[("m", _("Male")), ("f", _("Female"))], choices=[("m", _("Male")), ("f", _("Female"))],
@ -1061,6 +1065,14 @@ class Customer(models.Model):
address = models.CharField( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField(
upload_to="customers/", blank=True, null=True, verbose_name=_("Image")
)
customer_type = models.CharField(
choices=[("customer", _("Customer")),("organization", _("Organization"))], default="customer", max_length=15, verbose_name=_("Customer Type"),
null=True,blank=True
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
@ -1069,13 +1081,64 @@ class Customer(models.Model):
verbose_name_plural = _("Customers") verbose_name_plural = _("Customers")
def __str__(self): def __str__(self):
middle = f" {self.middle_name}" if self.middle_name else "" # middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name}{middle} {self.last_name}" return f"{self.first_name} {self.last_name}"
@property @property
def get_full_name(self): def full_name(self):
return f"{self.first_name} {self.middle_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
def create_customer_model(self):
customer_dict = to_dict(self)
customer = self.dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": self.full_name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
},
)
try:
customer.additional_info.update({"customer_info": customer_dict})
customer.additional_info.update({"type": "customer"})
except Exception:
pass
customer.save()
return customer
def update_user_model(self):
user = self.user
user.first_name = self.first_name
user.last_name = self.last_name
user.email = self.email
user.save()
return user
def update_customer_model(self):
customer_dict = to_dict(self)
customer = self.customer_model
customer.customer_name = self.full_name
customer.address_1 = self.address
customer.phone = self.phone_number
customer.email = self.email
try:
customer.additional_info.update({"customer_info": customer_dict})
customer.additional_info.update({"type": "customer"})
except Exception:
pass
customer.save()
return customer
def create_user_model(self):
user = User.objects.create_user(
username=self.email,
email=self.email,
first_name=self.first_name,
last_name=self.last_name,
)
user.set_password("Tenhal@123")
user.save()
return user
class Organization(models.Model, LocalizedNameMixin): class Organization(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey( dealer = models.ForeignKey(

View File

@ -195,39 +195,6 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
else: else:
instance.update_vendor_model() instance.update_vendor_model()
@receiver(post_save, sender=models.CustomerModel)
def create_customer_user(sender, instance, created, **kwargs):
"""
Connects to the `post_save` signal of the `CustomerModel` to create a user object
associated with the customer instance whenever a new `CustomerModel` instance is
created. Retrieves customer-specific information from `additional_info` to initialize
and configure the associated user object. Ensures the user object created has
unusable passwords by default.
:param sender: The model class that sends the signal (`CustomerModel`).
:param instance: The instance of `CustomerModel` that triggered the signal.
:param created: A boolean indicating whether a new instance was created.
:param kwargs: Additional keyword arguments passed by the signal.
:return: None
"""
if created:
try:
first_name = instance.additional_info.get("customer_info").get("first_name")
last_name = instance.additional_info.get("customer_info").get("last_name")
user = User.objects.create(
username=instance.email,
email=instance.email,
first_name=first_name if first_name else '',
last_name=last_name if last_name else '',
)
instance.additional_info.update({"user_info": to_dict(user)})
user.set_unusable_password()
user.save()
instance.user = user
instance.save()
except Exception as e:
print(e)
# Create Item # Create Item
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
def create_item_model(sender, instance, created, **kwargs): def create_item_model(sender, instance, created, **kwargs):
@ -769,7 +736,13 @@ def update_finance_cost(sender, instance, created, **kwargs):
if created: if created:
entity = instance.car.dealer.entity entity = instance.car.dealer.entity
vendor = instance.car.vendor vendor = instance.car.vendor
name = f"{instance.car.vin}-{instance.car.id_car_make.name}-{instance.car.id_car_model.name}-{instance.car.year}-{vendor.vendor_name}" vin = instance.car.vin if instance.car.vin else ""
make = instance.car.id_car_make.name if instance.car.id_car_make else ""
model = instance.car.id_car_model.name if instance.car.id_car_model else ""
year = instance.car.year
vendor_name = vendor.name if vendor else ""
name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity) ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity)
save_journal(instance,ledger,vendor) save_journal(instance,ledger,vendor)

View File

@ -76,26 +76,26 @@ urlpatterns = [
# CRM URLs # CRM URLs
path("customers/", views.CustomerListView.as_view(), name="customer_list"), path("customers/", views.CustomerListView.as_view(), name="customer_list"),
path( path(
"customers/<uuid:pk>/", "customers/<int:pk>/",
views.CustomerDetailView.as_view(), views.CustomerDetailView.as_view(),
name="customer_detail", name="customer_detail",
), ),
path( path(
"customers/create/", views.CustomerCreateView, name="customer_create" "customers/create/", views.CustomerCreateView.as_view(), name="customer_create"
), ),
path( path(
"customers/<uuid:pk>/update/", "customers/<int:pk>/update/",
views.CustomerUpdateView, views.CustomerUpdateView.as_view(),
name="customer_update", name="customer_update",
), ),
path("customers/<uuid:pk>/delete/", views.delete_customer, name="customer_delete"), path("customers/<int:pk>/delete/", views.delete_customer, name="customer_delete"),
path( path(
"customers/<str:customer_id>/opportunities/create/", "customers/<str:customer_id>/opportunities/create/",
views.OpportunityCreateView.as_view(), views.OpportunityCreateView.as_view(),
name="create_opportunity", name="create_opportunity",
), ),
path( path(
"customers/<uuid:pk>/add-note/", "customers/<int:pk>/add-note/",
views.add_note_to_customer, views.add_note_to_customer,
name="add_note_to_customer", name="add_note_to_customer",
), ),
@ -380,7 +380,7 @@ path(
), ),
path( path(
"organizations/create/", "organizations/create/",
views.OrganizationCreateView, views.OrganizationCreateView.as_view(),
name="organization_create", name="organization_create",
), ),
path( path(

View File

@ -388,7 +388,7 @@ def get_financial_values(model):
if i.item_model.additional_info["additional_services"]: if i.item_model.additional_info["additional_services"]:
additional_services.extend( additional_services.extend(
[ [
{"name": x.name, "price": x.price} {"name": x['name'], "price": x["price"]}
for x in i.item_model.additional_info["additional_services"] for x in i.item_model.additional_info["additional_services"]
] ]
) )
@ -1042,6 +1042,7 @@ class CarFinanceCalculator:
total_vat_amount = total_price_discounted * self.vat_rate total_vat_amount = total_price_discounted * self.vat_rate
return { return {
"total_price_before_discount": round(total_price, 2), # total_price_before_discount,
"total_price": round(total_price_discounted, 2), # total_price_discounted, "total_price": round(total_price_discounted, 2), # total_price_discounted,
"total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount,
"total_discount": round(total_discount,2), "total_discount": round(total_discount,2),
@ -1055,6 +1056,7 @@ class CarFinanceCalculator:
"cars": [self._get_car_data(item) for item in self.item_transactions], "cars": [self._get_car_data(item) for item in self.item_transactions],
"quantity": sum(self._get_quantity(item) for item in self.item_transactions), "quantity": sum(self._get_quantity(item) for item in self.item_transactions),
"total_price": totals['total_price'], "total_price": totals['total_price'],
"total_price_before_discount": totals['total_price_before_discount'],
"total_vat": totals['total_vat_amount'] + totals['total_price'], "total_vat": totals['total_vat_amount'] + totals['total_price'],
"total_vat_amount": totals['total_vat_amount'], "total_vat_amount": totals['total_vat_amount'],
"total_discount": totals['total_discount'], "total_discount": totals['total_discount'],

View File

@ -0,0 +1,9 @@
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
class SaudiPhoneNumberValidator(RegexValidator):
def __init__(self):
super().__init__(
regex=r'^(\+9665|05)[0-9]{8}$',
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)")
)

View File

@ -1884,7 +1884,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
:ivar permission_required: A list of permissions required to access the view. :ivar permission_required: A list of permissions required to access the view.
:type permission_required: list :type permission_required: list
""" """
model = CustomerModel model = models.Customer
home_label = _("customers") home_label = _("customers")
context_object_name = "customers" context_object_name = "customers"
paginate_by = 10 paginate_by = 10
@ -1895,9 +1895,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self): def get_queryset(self):
query = self.request.GET.get("q") query = self.request.GET.get("q")
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
customers = dealer.entity.get_customers().filter( customers = dealer.customers.all()
additional_info__type="customer"
)
return apply_search_filters(customers, query) return apply_search_filters(customers, query)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -1923,7 +1921,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
:ivar permission_required: The list of permissions required to access this view. :ivar permission_required: The list of permissions required to access this view.
:type permission_required: list[str] :type permission_required: list[str]
""" """
model = CustomerModel model = models.Customer
template_name = "customers/view_customer.html" template_name = "customers/view_customer.html"
context_object_name = "customer" context_object_name = "customer"
permission_required = ["django_ledger.view_customermodel"] permission_required = ["django_ledger.view_customermodel"]
@ -1935,9 +1933,9 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
context["customer_notes"] = models.Notes.objects.filter( context["customer_notes"] = models.Notes.objects.filter(
object_id=self.object.pk object_id=self.object.pk
) )
estimates = entity.get_estimates().filter(customer=self.object) estimates = entity.get_estimates().filter(customer=self.object.customer_model)
invoices = entity.get_invoices().filter(customer=self.object) invoices = entity.get_invoices().filter(customer=self.object.customer_model)
# txs = entity. transactions(customer=self.object)
total = estimates.count() + invoices.count() total = estimates.count() + invoices.count()
context["estimates"] = estimates context["estimates"] = estimates
@ -1947,7 +1945,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
@login_required @login_required
def add_note_to_customer(request, customer_id): def add_note_to_customer(request, pk):
""" """
This function allows authenticated users to add a note to a specific customer. The This function allows authenticated users to add a note to a specific customer. The
note creation is handled by a form, which is validated after submission. If the form note creation is handled by a form, which is validated after submission. If the form
@ -1964,7 +1962,7 @@ def add_note_to_customer(request, customer_id):
POST request, it renders the note form template with context including POST request, it renders the note form template with context including
the form and customer. the form and customer.
""" """
customer = get_object_or_404(CustomerModel, pk=customer_id) customer = get_object_or_404(models.Customer, pk=pk)
if request.method == "POST": if request.method == "POST":
form = forms.NoteForm(request.POST) form = forms.NoteForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -2018,131 +2016,163 @@ def add_activity_to_customer(request, pk):
) )
@login_required class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@permission_required("django_ledger.add_customermodel", raise_exception=True) model = models.Customer
def CustomerCreateView(request): form_class = forms.CustomerForm
""" permission_required = ["django_ledger.add_customermodel"]
Handles the creation of a new customer within the system. This view ensures that proper permissions template_name = "customers/customer_form.html"
and request methods are utilized. It provides feedback to the user about the success or failure of success_url = reverse_lazy("customer_list")
the customer creation process. When the form is submitted and valid, it checks for duplicate success_message = "Customer created successfully"
customers based on the email provided before proceeding with the customer creation.
:param request: The HTTP request object containing metadata about the request initiated by the user. def form_valid(self, form):
:type request: HttpRequest dealer = get_user_type(self.request)
:return: The rendered form page or a redirect to the customer list page upon successful creation. form.instance.dealer = dealer
:rtype: HttpResponse user = form.instance.create_user_model()
:raises PermissionDenied: If the user does not have the required permissions to access the view. customer = form.instance.create_customer_model()
"""
form = forms.CustomerForm()
if request.method == "POST":
form = forms.CustomerForm(request.POST)
dealer = get_user_type(request)
if form.is_valid(): form.instance.user = user
if ( form.instance.customer_model = customer
dealer.entity.get_customers()
.filter(email=form.cleaned_data["email"])
.exists()
):
messages.error(request, _("Customer with this email already exists"))
else:
customer_name = (
f"{form.cleaned_data['first_name']} "
f"{form.cleaned_data['last_name']}"
)
customer_dict = {
x: request.POST[x]
for x in request.POST
if x != "csrfmiddlewaretoken"
}
try:
customer = dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": customer_name,
"address_1": form.cleaned_data["address"],
"phone": form.cleaned_data["phone_number"],
"email": form.cleaned_data["email"],
},
)
customer.additional_info.update({"customer_info": customer_dict})
customer.additional_info.update({"type": "customer"})
customer.save()
messages.success(request, _("Customer created successfully")) return super().form_valid(form)
return redirect("customer_list")
except Exception as e: # @login_required
messages.error(request, _(f"An error occurred: {str(e)}")) # @permission_required("django_ledger.add_customermodel", raise_exception=True)
else: # def CustomerCreateView(request):
messages.error(request, _("Please correct the errors below")) # """
# Handles the creation of a new customer within the system. This view ensures that proper permissions
# and request methods are utilized. It provides feedback to the user about the success or failure of
# the customer creation process. When the form is submitted and valid, it checks for duplicate
# customers based on the email provided before proceeding with the customer creation.
return render(request, "customers/customer_form.html", {"form": form}) # :param request: The HTTP request object containing metadata about the request initiated by the user.
# :type request: HttpRequest
# :return: The rendered form page or a redirect to the customer list page upon successful creation.
# :rtype: HttpResponse
# :raises PermissionDenied: If the user does not have the required permissions to access the view.
# """
# form = forms.CustomerForm()
# if request.method == "POST":
# form = forms.CustomerForm(request.POST)
# dealer = get_user_type(request)
# if form.is_valid():
# if (
# dealer.entity.get_customers()
# .filter(email=form.cleaned_data["email"])
# .exists()
# ):
# messages.error(request, _("Customer with this email already exists"))
# else:
# customer_name = (
# f"{form.cleaned_data['first_name']} "
# f"{form.cleaned_data['last_name']}"
# )
# customer_dict = {
# x: request.POST[x]
# for x in request.POST
# if x != "csrfmiddlewaretoken"
# }
# try:
# customer = dealer.entity.create_customer(
# commit=False,
# customer_model_kwargs={
# "customer_name": customer_name,
# "address_1": form.cleaned_data["address"],
# "phone": form.cleaned_data["phone_number"],
# "email": form.cleaned_data["email"],
# },
# )
# customer.additional_info.update({"customer_info": customer_dict})
# customer.additional_info.update({"type": "customer"})
# customer.save()
# messages.success(request, _("Customer created successfully"))
# return redirect("customer_list")
# except Exception as e:
# messages.error(request, _(f"An error occurred: {str(e)}"))
# else:
# messages.error(request, _("Please correct the errors below"))
# return render(request, "customers/customer_form.html", {"form": form})
@login_required class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
@permission_required("django_ledger.change_customermodel", raise_exception=True) model = models.Customer
def CustomerUpdateView(request, pk): form_class = forms.CustomerForm
""" permission_required = ["django_ledger.change_customermodel"]
Updates the details of an existing customer in the database. This view is template_name = "customers/customer_form.html"
accessible only to logged-in users with the appropriate permissions. It success_url = reverse_lazy("customer_list")
handles both GET (form rendering with pre-filled customer data) and POST success_message = "Customer updated successfully"
(submitting updates) requests. Data validation and customer updates are
conducted based on the received form data.
:param request: The HTTP request object used to determine the request method, def form_valid(self, form):
access user session details, and provide request data such as POST content. form.instance.update_user_model()
Expected to contain the updated customer data if request method is POST. form.instance.update_customer_model()
:type request: HttpRequest return super().form_valid(form)
:param pk: The primary key of the CustomerModel object that is to be updated. # @login_required
:type pk: int # @permission_required("django_ledger.change_customermodel", raise_exception=True)
# def CustomerUpdateView(request, pk):
# """
# Updates the details of an existing customer in the database. This view is
# accessible only to logged-in users with the appropriate permissions. It
# handles both GET (form rendering with pre-filled customer data) and POST
# (submitting updates) requests. Data validation and customer updates are
# conducted based on the received form data.
:return: A rendered HTML template displaying the customer form pre-filled # :param request: The HTTP request object used to determine the request method,
with existing data if a GET request is received. On successful form # access user session details, and provide request data such as POST content.
submission (POST request), redirects to the customer list page # Expected to contain the updated customer data if request method is POST.
and displays a success message. In case of invalid data or errors, # :type request: HttpRequest
returns the rendered form template with the validation errors.
:rtype: HttpResponse
"""
customer = get_object_or_404(CustomerModel, pk=pk)
if request.method == "POST":
# form = forms.CustomerForm(request.POST, instance=customer)
customer_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
dealer = get_user_type(request)
customer_name = customer_dict["first_name"] + " " + customer_dict["last_name"]
instance = dealer.entity.get_customers().get(pk=pk) # :param pk: The primary key of the CustomerModel object that is to be updated.
instance.customer_name = customer_name # :type pk: int
instance.address_1 = customer_dict["address"]
instance.phone = customer_dict["phone_number"]
instance.email = customer_dict["email"]
customer_dict["pk"] = str(instance.pk) # :return: A rendered HTML template displaying the customer form pre-filled
instance.additional_info.update({"customer_info": customer_dict}) # with existing data if a GET request is received. On successful form
try: # submission (POST request), redirects to the customer list page
user = User.objects.filter( # and displays a success message. In case of invalid data or errors,
pk=int(instance.additional_info["user_info"]["id"]) # returns the rendered form template with the validation errors.
).first() # :rtype: HttpResponse
if user: # """
user.username = customer_dict["email"] # customer = get_object_or_404(CustomerModel, pk=pk)
user.email = customer_dict["email"] # if request.method == "POST":
user.save() # # form = forms.CustomerForm(request.POST, instance=customer)
except Exception as e: # customer_dict = {
raise Exception(e) # x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
# }
# dealer = get_user_type(request)
# customer_name = customer_dict["first_name"] + " " + customer_dict["last_name"]
instance.save() # instance = dealer.entity.get_customers().get(pk=pk)
messages.success(request, _("Customer updated successfully")) # instance.customer_name = customer_name
return redirect("customer_list") # instance.address_1 = customer_dict["address"]
else: # instance.phone = customer_dict["phone_number"]
form = forms.CustomerForm( # instance.email = customer_dict["email"]
initial=customer.additional_info["customer_info"]
if "customer_info" in customer.additional_info # customer_dict["pk"] = str(instance.pk)
else {} # instance.additional_info.update({"customer_info": customer_dict})
) # try:
return render(request, "customers/customer_form.html", {"form": form}) # user = User.objects.filter(
# pk=int(instance.additional_info["user_info"]["id"])
# ).first()
# if user:
# user.username = customer_dict["email"]
# user.email = customer_dict["email"]
# user.save()
# except Exception as e:
# raise Exception(e)
# instance.save()
# messages.success(request, _("Customer updated successfully"))
# return redirect("customer_list")
# else:
# form = forms.CustomerForm(
# initial=customer.additional_info["customer_info"]
# if "customer_info" in customer.additional_info
# else {}
# )
# return render(request, "customers/customer_form.html", {"form": form})
@login_required @login_required
@ -2162,9 +2192,11 @@ def delete_customer(request, pk):
:return: A redirect response to the customer list page. :return: A redirect response to the customer list page.
:rtype: HttpResponseRedirect :rtype: HttpResponseRedirect
""" """
customer = get_object_or_404(models.CustomerModel, pk=pk) customer = get_object_or_404(models.Customer, pk=pk)
user = User.objects.get(email=customer.email) customer_model = customer.customer_model
user = customer.user
customer.active = False customer.active = False
customer_model.active = False
user.is_active = False user.is_active = False
customer.save() customer.save()
user.save() user.save()
@ -2838,59 +2870,77 @@ class OrganizationDetailView(LoginRequiredMixin, DetailView):
context_object_name = "organization" context_object_name = "organization"
@login_required class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
def OrganizationCreateView(request): model = models.Organization
""" form_class = forms.OrganizationForm
Handles the creation of a new organization via a web form. This view allows the permission_required = ["django_ledger.add_customermodel"]
authenticated user to submit data for creating an organization. If a POST request template_name = "customers/organization_form.html"
is received, it validates the data, checks for duplicate organizations, and success_url = reverse_lazy("organization_list")
creates a customer linked to the organization, including its associated success_message = "Customer created successfully"
information such as address, phone number, and logo. Upon success, the user
is redirected to the organization list, and a success message is displayed.
:param request: The HTTP request object containing data for creating an organization. def form_valid(self, form):
:type request: HttpRequest dealer = get_user_type(self.request)
:return: An HTTP response object rendering the organization create form page or form.instance.dealer = dealer
redirecting the user after a successful creation. user = form.instance.create_user_model()
:rtype: HttpResponse customer = form.instance.create_customer_model()
""" form.instance.customer_type = 'organization'
if request.method == "POST": form.instance.user = user
form = forms.OrganizationForm(request.POST) form.instance.customer_model = customer
if CustomerModel.objects.filter(email=request.POST["email"]).exists():
messages.error(
request, _("An organization with this email already exists.")
)
return redirect("organization_create")
organization_dict = { return super().form_valid(form)
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken" # @login_required
} # def OrganizationCreateView(request):
dealer = get_user_type(request) # """
name = organization_dict["first_name"] + " " + organization_dict["last_name"] # Handles the creation of a new organization via a web form. This view allows the
customer = dealer.entity.create_customer( # authenticated user to submit data for creating an organization. If a POST request
commit=False, # is received, it validates the data, checks for duplicate organizations, and
customer_model_kwargs={ # creates a customer linked to the organization, including its associated
"customer_name": name, # information such as address, phone number, and logo. Upon success, the user
"address_1": organization_dict["address"], # is redirected to the organization list, and a success message is displayed.
"phone": organization_dict["phone_number"],
"email": organization_dict["email"],
},
)
image = request.FILES.get("logo")
if image:
file_name = default_storage.save("images/{}".format(image.name), image)
file_url = default_storage.url(file_name)
organization_dict["logo"] = file_url # :param request: The HTTP request object containing data for creating an organization.
organization_dict["pk"] = str(customer.pk) # :type request: HttpRequest
customer.additional_info.update({"customer_info": organization_dict}) # :return: An HTTP response object rendering the organization create form page or
customer.additional_info.update({"type": "organization"}) # redirecting the user after a successful creation.
customer.save() # :rtype: HttpResponse
messages.success(request, _("Organization created successfully")) # """
return redirect("organization_list") # if request.method == "POST":
else: # form = forms.OrganizationForm(request.POST)
form = forms.OrganizationForm() # if CustomerModel.objects.filter(email=request.POST["email"]).exists():
return render(request, "organizations/organization_form.html", {"form": form}) # messages.error(
# request, _("An organization with this email already exists.")
# )
# return redirect("organization_create")
# organization_dict = {
# x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
# }
# dealer = get_user_type(request)
# name = organization_dict["first_name"] + " " + organization_dict["last_name"]
# customer = dealer.entity.create_customer(
# commit=False,
# customer_model_kwargs={
# "customer_name": name,
# "address_1": organization_dict["address"],
# "phone": organization_dict["phone_number"],
# "email": organization_dict["email"],
# },
# )
# image = request.FILES.get("logo")
# if image:
# file_name = default_storage.save("images/{}".format(image.name), image)
# file_url = default_storage.url(file_name)
# organization_dict["logo"] = file_url
# organization_dict["pk"] = str(customer.pk)
# customer.additional_info.update({"customer_info": organization_dict})
# customer.additional_info.update({"type": "organization"})
# customer.save()
# messages.success(request, _("Organization created successfully"))
# return redirect("organization_list")
# else:
# form = forms.OrganizationForm()
# return render(request, "organizations/organization_form.html", {"form": form})
@login_required @login_required
@ -3639,8 +3689,9 @@ def create_estimate(request, pk=None):
data = json.loads(request.body) data = json.loads(request.body)
title = data.get("title") title = data.get("title")
customer_id = data.get("customer") customer_id = data.get("customer")
terms = data.get("terms") # terms = data.get("terms")
customer = entity.get_customers().filter(pk=customer_id).first() # customer = entity.get_customers().filter(pk=customer_id).first()
customer = models.Customer.objects.filter(pk=customer_id).first()
items = data.get("item", []) items = data.get("item", [])
quantities = data.get("quantity", []) quantities = data.get("quantity", [])
@ -3679,7 +3730,7 @@ def create_estimate(request, pk=None):
{"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")}, {"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")},
) )
estimate = entity.create_estimate( estimate = entity.create_estimate(
estimate_title=title, customer_model=customer, contract_terms=terms estimate_title=title, customer_model=customer.customer_model, contract_terms="fixed"
) )
if isinstance(items, list): if isinstance(items, list):
item_quantity_map = {} item_quantity_map = {}
@ -3773,12 +3824,11 @@ def create_estimate(request, pk=None):
} }
) )
form = forms.EstimateModelCreateForm( # form = forms.EstimateModelCreateForm(
entity_slug=entity.slug, user_model=entity.admin # entity_slug=entity.slug, user_model=entity.admin
) # )
form.fields["customer"].queryset = entity.get_customers().filter( form = forms.EstimateModelCreateForm()
active=True, additional_info__type="customer" form.fields["customer"].queryset = dealer.customers.all()
)
if pk: if pk:
opportunity = models.Opportunity.objects.get(pk=pk) opportunity = models.Opportunity.objects.get(pk=pk)
@ -3988,13 +4038,9 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
estimate = kwargs.get("object") estimate = kwargs.get("object")
if estimate.get_itemtxs_data(): if estimate.get_itemtxs_data():
data = get_financial_values(estimate) # data = get_financial_values(estimate)
calculator = CarFinanceCalculator(estimate)
kwargs["vat_amount"] = data["vat_amount"] kwargs["data"] = calculator.get_finance_data()
kwargs["total"] = data["grand_total"]
kwargs["discount_amount"] = data["discount_amount"]
kwargs["vat"] = data["vat"]
kwargs["additional_services"] = data["additional_services"]
kwargs["dealer"] = dealer kwargs["dealer"] = dealer
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -6221,11 +6267,12 @@ def send_email_view(request, pk):
{dealer.phone_number} {dealer.phone_number}
هيكل | Haikal هيكل | Haikal
""" """
subject = _("Quotation") # subject = _("Quotation")
send_email( send_email(
str(settings.DEFAULT_FROM_EMAIL), str(settings.DEFAULT_FROM_EMAIL),
estimate.customer.email, estimate.customer.email,
subject, "عرض سعر - Quotation",
msg, msg,
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -22,7 +22,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm-6 col-md-8"> <div class="col-sm-6 col-md-8">
<form method="post" class="form row g-3 needs-validation" novalidate> <form method="post" class="form row g-3 needs-validation" enctype="multipart/form-data" novalidate>
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="col-12"> <div class="col-12">

View File

@ -70,17 +70,17 @@
</td> </td>
<td class="name align-middle white-space-nowrap ps-0"> <td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.pk %}">{{ customer.customer_name }}</a> <div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.pk %}">{{ customer.full_name }}</a>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ customer.email }}</a></td> <td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ customer.email }}</a></td>
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone }}</a></td> <td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a></td>
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">{{ customer.additional_info.customer_info.national_id }}</td> <td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">{{ customer.national_id }}</td>
<td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight"> <td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.address_1 }}</td> {{ customer.address }}</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> <td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{% if customer.active %} {% if customer.active %}
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{customer.active}}</span> <span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{customer.active}}</span>

View File

@ -41,10 +41,10 @@
<div class="card-body d-flex flex-column justify-content-between pb-3"> <div class="card-body d-flex flex-column justify-content-between pb-3">
<div class="row align-items-center g-5 mb-3 text-center text-sm-start"> <div class="row align-items-center g-5 mb-3 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-sm-2"> <div class="col-12 col-sm-auto mb-sm-2">
<div class="avatar avatar-5xl"><img class="rounded-circle" src="{% static "images/team/15.webp" %}" alt="" /></div> <div class="avatar avatar-5xl"><img class="rounded-circle" src="{{ customer.image.url }}" alt="" /></div>
</div> </div>
<div class="col-12 col-sm-auto flex-1"> <div class="col-12 col-sm-auto flex-1">
<h3>{{ customer.customer_name }}</h3> <h3>{{ customer.full_name }}</h3>
<p class="text-body-secondary">{{ customer.created|timesince}}</p> <p class="text-body-secondary">{{ customer.created|timesince}}</p>
</div> </div>
</div> </div>
@ -69,11 +69,11 @@
<button class="btn btn-link p-0"><span class="fas fa-pen fs-8 ms-3 text-body-quaternary"></span></button> <button class="btn btn-link p-0"><span class="fas fa-pen fs-8 ms-3 text-body-quaternary"></span></button>
</div> </div>
<h5 class="text-body-secondary">{{ _("Address") }}</h5> <h5 class="text-body-secondary">{{ _("Address") }}</h5>
<p class="text-body-secondary">{{ customer.address_1}}</p> <p class="text-body-secondary">{{ customer.address}}</p>
<div class="mb-3"> <div class="mb-3">
<h5 class="text-body-secondary">{% trans 'Email' %}</h5><a href="{{ customer.email}}">{{ customer.email }}</a> <h5 class="text-body-secondary">{% trans 'Email' %}</h5><a href="{{ customer.email}}">{{ customer.email }}</a>
</div> </div>
<h5 class="text-body-secondary">{% trans 'Phone Number' %}</h5><a class="text-body-secondary" href="#">{{ customer.phone }}</a> <h5 class="text-body-secondary">{% trans 'Phone Number' %}</h5><a class="text-body-secondary" href="#">{{ customer.phone_number }}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -133,7 +133,6 @@
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value, csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
title: document.querySelector('[name=title]').value, title: document.querySelector('[name=title]').value,
customer: document.querySelector('[name=customer]').value, customer: document.querySelector('[name=customer]').value,
terms: document.querySelector('[name=terms]').value,
item: [], item: [],
quantity: [], quantity: [],
opportunity_id: "{{opportunity_id}}" opportunity_id: "{{opportunity_id}}"

View File

@ -99,23 +99,25 @@
<td></td> <td></td>
<td> <td>
<div class="qr-code"> <div class="qr-code">
<img class="rounded-soft" src="{{ dealer.logo.url|default:'' }}" alt="Dealer Logo"/> {% if dealer.logo %}
<img class="rounded-soft" src="{{ dealer.logo.url|default:'' }}" alt="Dealer Logo"/>
{% endif %}
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td><strong>{{ dealer.name }}</strong></td> <td class="ps-1"><strong>Customer Name</strong></td>
<td></td> <td class="text-center">{{ dealer.arabic_name }}<br>{{ dealer.name }}</td>
<td class="text-end"><strong>{{ dealer.arabic_name }}</strong></td> <td class="text-end"><strong>{{ dealer.arabic_name }}</strong></td>
</tr> </tr>
<tr> <tr>
<td><strong>Address</strong></td> <td class="ps-1"><strong>Address</strong></td>
<td>{{ dealer.address }}</td> <td class="text-center">{{ dealer.address }}</td>
<td class="text-end"> <strong>العنوان</strong></td> <td class="text-end"> <strong>العنوان</strong></td>
</tr> </tr>
<tr> <tr>
<td><strong>Phone</strong></td> <td class="ps-1"><strong>Phone</strong></td>
<td>{{ dealer.phone_number }}</td> <td class="text-center">{{ dealer.phone_number }}</td>
<td class="text-end"><strong>جوال</strong></td> <td class="text-end"><strong>جوال</strong></td>
</tr> </tr>
<tr> <tr>
@ -190,8 +192,8 @@
<tr> <tr>
<td class="ps-1 fs-10 align-content-center" colspan="5"></td> <td class="ps-1 fs-10 align-content-center" colspan="5"></td>
<td class="text-center fs-10 align-content-center">{{ data.quantity|floatformat:-1 }}</td> <td class="text-center fs-10 align-content-center">{{ data.quantity|floatformat:-1 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.total_price|floatformat:2 }}</td> <td class="text-center fs-10 align-content-center">{{ data.total_price_before_discount|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.total_vat|floatformat:2 }}</td> <td class="text-center fs-10 align-content-center">{{ data.grand_total|floatformat:2 }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -96,46 +96,40 @@
<h5 class="fs-5">Tax&nbsp;Invoice&nbsp;&nbsp;/&nbsp;&nbsp;فاتورة&nbsp;ضريبية</h5> <h5 class="fs-5">Tax&nbsp;Invoice&nbsp;&nbsp;/&nbsp;&nbsp;فاتورة&nbsp;ضريبية</h5>
</div> </div>
<div class="invoice-details p-1"> <div class="invoice-details p-1">
<table class="table table-sm table-responsive border-gray-50"> <div class="d-flex justify-content-center align-items-center">
<div class="qr-code">
<img src="{% static 'qr_code/Marwan_qr.png' %}" alt="QR Code">
</div>
</div>
<div class="d-flex justify-content-end align-items-end">
<div class="dealer-logo ">
{% if dealer.logo %}
<img class="rounded-soft" src="{{ dealer.logo.url|default:'' }}" alt="Dealer Logo"/>
{% endif %}
</div>
</div>
<table class="table table-sm table-bordered border-gray-50">
<tr> <tr>
<td> <td class="ps-1"><strong>Customer Name</strong></td>
<div class="d-flex justify-content-center align-items-center"> <td class="text-center">{{ dealer.arabic_name }}<br>{{ dealer.name }}</td>
<div class="qr-code"> <td class="text-end"><strong>اسم&nbsp;العميل</strong></td>
<img src="{% static 'qr_code/Marwan_qr.png' %}" alt="QR Code"> </tr>
</div> <tr>
</div> <td class="ps-1"><strong>Address</strong></td>
</td> <td class="text-center">{{ dealer.address }}</td>
<td></td> <td class="text-end"> <strong>العنوان</strong></td>
<td> </tr>
<div class="d-flex justify-content-end align-items-end"> <tr>
<div class="dealer-logo "> <td class="ps-1"><strong>Phone</strong></td>
{% if dealer.logo %} <td class="text-center">{{ dealer.phone_number }}</td>
<img class="rounded-soft" src="{{ dealer.logo.url|default:'' }}" alt="Dealer Logo"/> <td class="text-end"><strong>جوال</strong></td>
{% endif %} </tr>
</div> <tr>
</div> <td class="ps-1"><strong>VAT Number</strong></td>
</td> <td class="text-center">{{ dealer.vrn }}</td>
</tr> <td class="text-end p-1"><strong>الرقم الضريبي</strong></td>
<tr> </tr>
<td><strong>{{ dealer.name }}</strong></td>
<td></td>
<td class="text-end"><strong>{{ dealer.arabic_name }}</strong></td>
</tr>
<tr>
<td><strong>Address</strong></td>
<td>{{ dealer.address }}</td>
<td class="text-end"><strong>العنوان</strong></td>
</tr>
<tr>
<td><strong>Phone</strong></td>
<td>{{ dealer.phone_number }}</td>
<td class="text-end"><strong>جوال</strong></td>
</tr>
<tr>
<td><strong>VAT Number</strong></td>
<td>{{ dealer.vrn }}</td>
<td class="text-end"><strong>الرقم الضريبي</strong></td>
</tr>
</table> </table>
<table class="table table-sm table-bordered border-gray-50"> <table class="table table-sm table-bordered border-gray-50">
@ -203,8 +197,8 @@
<tr> <tr>
<td class="ps-1 fs-10 align-content-center" colspan="5"></td> <td class="ps-1 fs-10 align-content-center" colspan="5"></td>
<td class="text-center fs-10 align-content-center">{{ data.quantity|floatformat }}</td> <td class="text-center fs-10 align-content-center">{{ data.quantity|floatformat }}</td>
<td class="text-center fs-10 align-content-center">{{ data.total_price|floatformat }}</td> <td class="text-center fs-10 align-content-center">{{ data.total_price_before_discount|floatformat }}</td>
<td class="text-center fs-10 align-content-center">{{ data.total_vat|floatformat }}</td> <td class="text-center fs-10 align-content-center">{{ data.grand_total|floatformat }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>