Compare commits

...

3 Commits

Author SHA1 Message Date
ismail
5dee96ec09 update 2025-05-07 14:35:41 +03:00
ismail
b44fb270fa update the preview 2025-05-07 14:35:41 +03:00
ismail
9d6d69f1d0 fix vendor 2025-05-07 14:35:41 +03:00
33 changed files with 881 additions and 690 deletions

View File

@ -16,7 +16,6 @@ from django.utils.decorators import method_decorator
from inventory import models as inventory_models
class LoginView(APIView):
permission_classes = [permissions.AllowAny,]

View File

@ -10,6 +10,8 @@ from django.core.validators import MinLengthValidator
from django import forms
from plans.models import PlanPricing
from django.contrib.auth import get_user_model
from inventory.validators import SaudiPhoneNumberValidator
from .models import CustomGroup, Status, Stage
from .mixins import AddClassMixin
from django_ledger.forms.invoice import (
@ -46,6 +48,8 @@ from .models import (
CarModel,
SaleOrder,
CarMake,
Customer,
Organization,
DealerSettings
)
from django_ledger import models as ledger_models
@ -59,6 +63,13 @@ import django_tables2 as tables
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):
"""
A form used for creating and updating instances of the
@ -105,20 +116,14 @@ class StaffForm(forms.ModelForm):
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
queryset=Service.objects.all(),
required=False,)
phone_number = forms.CharField(
phone_number = SaudiPhoneNumberField(
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 (8-15 digits, starting with 05)')
)]
)
class Meta:
model = Staff
@ -148,7 +153,9 @@ class DealerForm(forms.ModelForm):
:type address: str
:ivar logo: Logo of the dealer.
:type logo: File
"""
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
class Meta:
model = Dealer
fields = [
@ -162,64 +169,81 @@ class DealerForm(forms.ModelForm):
]
class CustomerForm(forms.Form):
"""
Represents a form for collecting customer information.
class CustomerForm(forms.ModelForm):
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
This form is used to gather and validate customer details such as name,
email, phone number, national ID, tax registration details, and address.
It includes several fields with validation constraints and specific
requirements. It is designed to handle both required and optional fields,
ensuring the correctness of user inputs.
class Meta:
model = Customer
fields = [
'title',
"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.
:type first_name: forms.CharField
:ivar last_name: Customer's last name.
:type last_name: forms.CharField
:ivar arabic_name: Customer's name in Arabic.
:type arabic_name: forms.CharField
:ivar email: Customer's email address.
:type email: forms.EmailField
:ivar phone_number: Customer's phone number. Validates the format and
ensures the number is in the Saudi Arabia region.
:type phone_number: PhoneNumberField
:ivar national_id: Customer's national ID. Optional field limited to
a maximum length of 10 characters.
:type national_id: forms.CharField
:ivar crn: Commercial registration number (CRN) of the customer. Optional field.
:type crn: forms.CharField
:ivar vrn: Value-added tax registration number (VRN) of the customer.
Optional field.
:type vrn: forms.CharField
:ivar address: Customer's address.
:type address: forms.CharField
"""
first_name = forms.CharField()
last_name = forms.CharField()
arabic_name = forms.CharField()
email = forms.EmailField()
# phone_number = PhoneNumberField(
# label=_("Phone Number"),
# widget=forms.TextInput(
# attrs={
# "placeholder": _("Phone"),
# }
# ),
# region="SA",
# error_messages={
# "required": _("This field is required."),
# "invalid": _("Phone number must be in the format 05xxxxxxxx"),
# },
# 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()
# This form is used to gather and validate customer details such as name,
# email, phone number, national ID, tax registration details, and address.
# It includes several fields with validation constraints and specific
# requirements. It is designed to handle both required and optional fields,
# ensuring the correctness of user inputs.
# :ivar first_name: Customer's first name.
# :type first_name: forms.CharField
# :ivar last_name: Customer's last name.
# :type last_name: forms.CharField
# :ivar arabic_name: Customer's name in Arabic.
# :type arabic_name: forms.CharField
# :ivar email: Customer's email address.
# :type email: forms.EmailField
# :ivar phone_number: Customer's phone number. Validates the format and
# ensures the number is in the Saudi Arabia region.
# :type phone_number: PhoneNumberField
# :ivar national_id: Customer's national ID. Optional field limited to
# a maximum length of 10 characters.
# :type national_id: forms.CharField
# :ivar crn: Commercial registration number (CRN) of the customer. Optional field.
# :type crn: forms.CharField
# :ivar vrn: Value-added tax registration number (VRN) of the customer.
# Optional field.
# :type vrn: forms.CharField
# :ivar address: Customer's address.
# :type address: forms.CharField
# """
# first_name = forms.CharField()
# last_name = forms.CharField()
# arabic_name = forms.CharField()
# email = forms.EmailField()
# # phone_number = PhoneNumberField(
# # label=_("Phone Number"),
# # widget=forms.TextInput(
# # attrs={
# # "placeholder": _("Phone"),
# # }
# # ),
# # region="SA",
# # error_messages={
# # "required": _("This field is required."),
# # "invalid": _("Phone number must be in the format 05xxxxxxxx"),
# # },
# # 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(forms.ModelForm):
"""
Represents a form for collecting and handling organization-specific details.
@ -234,8 +258,10 @@ class OrganizationForm(CustomerForm):
:ivar logo: Optional field to upload the logo of the organization.
:type logo: forms.ImageField
"""
contact_person = forms.CharField(required=False)
logo = forms.ImageField(required=False)
phone_number = SaudiPhoneNumberField(label=_('Phone Number'),required=True)
class Meta:
model = Organization
fields = ["name","arabic_name","email","phone_number","crn","vrn","address","logo"]
class CarForm(
@ -292,10 +318,8 @@ class CarForm(
self.fields["id_car_model"].choices = [
(obj.id_car_model, obj.get_local_name()) for obj in queryset
]
if "vendor" in self.fields:
self.fields["vendor"].queryset = ledger_models.VendorModel.objects.filter(
active=True
)
# if "vendor" in self.fields:
# self.fields["vendor"].queryset = dealer.vendors.all()
class CarUpdateForm(forms.ModelForm, AddClassMixin):
@ -482,7 +506,7 @@ class VendorForm(forms.ModelForm):
:ivar Meta: Inner class to define metadata for the Vendor form.
:type Meta: Type[VendorForm.Meta]
"""
phone_number = forms.CharField(label=_("Phone Number"),min_length=10,max_length=10,validators=[RegexValidator(regex='^05[0-9]{8}$')], required=True)
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
contact_person = forms.CharField(label=_("Contact Person"))
class Meta:
@ -569,6 +593,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'))
class Meta:
model = Representative
fields = [
@ -787,21 +813,7 @@ class WizardForm2(forms.Form):
# },
# required=True,
# )
phone_number = forms.CharField(
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)')
)]
)
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
class WizardForm3(forms.Form):
@ -998,6 +1010,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'))
id_car_make = forms.ModelChoiceField(
label=_("Make"),
queryset=CarMake.objects.filter(is_sa_import=True),
@ -1205,71 +1219,75 @@ class SaleOrderForm(forms.ModelForm):
}
class EstimateModelCreateForm(EstimateModelCreateFormBase):
"""
Defines the EstimateModelCreateForm class, which is used to create and manage
forms for EstimateModel. This form handles the rendering and validation
of specific fields, their input widgets, and labels.
class EstimateModelCreateForm(forms.Form):
title = forms.CharField(max_length=255)
customer = forms.ModelChoiceField(queryset=Customer.objects.none())
The purpose of this class is to provide a structured way to handle
EstimateModel instances and their related fields, ensuring a user-friendly
form interface with proper field configuration. It facilitates fetching
related data, such as customer queries, based on user-specific parameters.
# class EstimateModelCreateForm(EstimateModelCreateFormBase):
# """
# Defines the EstimateModelCreateForm class, which is used to create and manage
# 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
form operates.
:type ENTITY_SLUG: str
:ivar USER_MODEL: The user model that provides methods and objects needed
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"),
}
# The purpose of this class is to provide a structured way to handle
# EstimateModel instances and their related fields, ensuring a user-friendly
# form interface with proper field configuration. It facilitates fetching
# related data, such as customer queries, based on user-specific parameters.
def __init__(self, *args, entity_slug, user_model, **kwargs):
super(EstimateModelCreateForm, self).__init__(
*args, entity_slug=entity_slug, user_model=user_model, **kwargs
)
self.ENTITY_SLUG = entity_slug
self.USER_MODEL = user_model
self.fields["customer"].queryset = self.get_customer_queryset()
# :ivar ENTITY_SLUG: A string that represents the entity context in which the
# form operates.
# :type ENTITY_SLUG: str
# :ivar USER_MODEL: The user model that provides methods and objects needed
# 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 get_customer_queryset(self):
return self.USER_MODEL.dealer.entity.get_customers()
# def __init__(self, *args, entity_slug, user_model, **kwargs):
# super(EstimateModelCreateForm, self).__init__(
# *args, entity_slug=entity_slug, user_model=user_model, **kwargs
# )
# self.ENTITY_SLUG = entity_slug
# self.USER_MODEL = user_model
# self.fields["customer"].queryset = self.get_customer_queryset()
# def get_customer_queryset(self):
# return self.USER_MODEL.dealer.entity.get_customers()
class OpportunityStatusForm(forms.Form):
@ -1581,21 +1599,7 @@ class PaymentPlanForm(forms.Form):
label=_('Email Address')
)
phone = forms.CharField(
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)')
)]
)
phone_number = SaudiPhoneNumberField(label=_('Phone Number'))
# Credit Card Fields (not saved to database)
card_number = CreditCardField(

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-05-04 16:07
# Generated by Django 5.1.7 on 2025-05-05 16:32
import datetime
import django.core.validators
@ -17,17 +17,11 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
# ('appointment', '0002_alter_workinghours_options'),
('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_ACCOUNT_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_CUSTOMER_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_ENTITY_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_ESTIMATE_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_INVOICE_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_ITEM_MODEL),
migrations.swappable_dependency(settings.DJANGO_LEDGER_VENDOR_MODEL),
]
operations = [
@ -92,7 +86,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('SADAD', 'SADAD')], max_length=50, verbose_name='method')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
],
@ -120,7 +114,7 @@ class Migration(migrations.Migration):
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.DJANGO_LEDGER_ITEM_MODEL, verbose_name='Item')),
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')),
],
options={
'verbose_name': 'Additional Services',
@ -140,7 +134,7 @@ class Migration(migrations.Migration):
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
('vendor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to=settings.DJANGO_LEDGER_VENDOR_MODEL, verbose_name='Vendor')),
('vendor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='django_ledger.vendormodel', verbose_name='Vendor')),
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
],
options={
@ -322,7 +316,7 @@ class Migration(migrations.Migration):
('logo', models.ImageField(blank=True, null=True, upload_to='logos/users', verbose_name='Logo')),
('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.DJANGO_LEDGER_ENTITY_MODEL)),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.entitymodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
],
options={
@ -339,7 +333,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='')),
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='inventory.dealer')),
],
),
@ -437,13 +431,13 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('additional_info', models.JSONField(blank=True, default=dict, null=True)),
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to='django_ledger.accountmodel')),
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to='django_ledger.accountmodel')),
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to='django_ledger.accountmodel')),
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')),
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to=settings.DJANGO_LEDGER_ACCOUNT_MODEL)),
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to='django_ledger.accountmodel')),
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to='django_ledger.accountmodel')),
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to='django_ledger.accountmodel')),
],
),
migrations.CreateModel(
@ -485,7 +479,7 @@ class Migration(migrations.Migration):
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('contacted', 'Contacted'), ('converted', 'Converted'), ('canceled', 'Canceled')], db_index=True, default='new', max_length=50, verbose_name='Status')),
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leads', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='django_ledger.customermodel')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
@ -584,12 +578,12 @@ class Migration(migrations.Migration):
name='SaleOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_method', models.CharField(choices=[('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer'), ('SADAD', 'SADAD')], max_length=20)),
('payment_method', models.CharField(choices=[('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer'), ('sadad', 'SADAD')], max_length=20)),
('comments', models.TextField(blank=True, null=True)),
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to=settings.DJANGO_LEDGER_ESTIMATE_MODEL, verbose_name='Estimate')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to=settings.DJANGO_LEDGER_INVOICE_MODEL, verbose_name='Invoice')),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.invoicemodel', verbose_name='Invoice')),
],
options={
'ordering': ['-created'],
@ -599,15 +593,15 @@ class Migration(migrations.Migration):
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purpose', models.CharField(choices=[('Product Demo', 'Product Demo'), ('Follow-Up Call', 'Follow-Up Call'), ('Contract Discussion', 'Contract Discussion'), ('Sales Meeting', 'Sales Meeting'), ('Support Call', 'Support Call'), ('Other', 'Other')], max_length=200)),
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
('scheduled_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('Call', 'Call'), ('Meeting', 'Meeting'), ('Email', 'Email')], default='Call', max_length=200)),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('Scheduled', 'Scheduled'), ('Completed', 'Completed'), ('Canceled', 'Canceled')], default='Scheduled', max_length=200)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
@ -650,9 +644,9 @@ class Migration(migrations.Migration):
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('closed', models.BooleanField(default=False, verbose_name='Closed')),
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='django_ledger.customermodel')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to=settings.DJANGO_LEDGER_ESTIMATE_MODEL)),
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner')),
],
@ -710,6 +704,7 @@ class Migration(migrations.Migration):
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
('vendor_model', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
],
options={
'verbose_name': 'Vendor',
@ -783,6 +778,7 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Payment History',
'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'],
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-05-06 12:57
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', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='vendor',
name='vendor_model',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-05-06 14:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_alter_vendor_vendor_model'),
]
operations = [
migrations.AlterField(
model_name='car',
name='vendor',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
),
]

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

@ -0,0 +1,32 @@
# Generated by Django 5.1.7 on 2025-05-07 09:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
('inventory', '0007_customer_customer_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='customer',
name='customer_type',
),
migrations.AddField(
model_name='organization',
name='customer_model',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel'),
),
migrations.AddField(
model_name='organization',
name='user',
field=models.OneToOneField(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-05-07 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_remove_customer_customer_type_and_more'),
]
operations = [
migrations.AddField(
model_name='organization',
name='email',
field=models.EmailField(default='t@tenhal.sa', max_length=254, verbose_name='Email'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-07 09:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_organization_email'),
]
operations = [
migrations.AddField(
model_name='organization',
name='active',
field=models.BooleanField(default=True, verbose_name='Active'),
),
]

View File

@ -19,7 +19,7 @@ from phonenumber_field.modelfields import PhoneNumberField
from django.utils.timezone import now
from django.contrib.auth.models import Group
from inventory.utils import get_user_type
from inventory.utils import get_user_type, to_dict
from .mixins import LocalizedNameMixin
from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel,AccountModel,EntityManagementModel
from django.contrib.contenttypes.fields import GenericForeignKey
@ -376,7 +376,7 @@ class Car(models.Model):
)
vendor = models.ForeignKey(
VendorModel,
"Vendor",
models.DO_NOTHING,
related_name="cars",
verbose_name=_("Vendor"),
@ -1032,18 +1032,22 @@ class Priority(models.TextChoices):
HIGH = "high", _("High")
class Customer(models.Model):
dealer = models.ForeignKey(
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')
title = models.CharField(
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
)
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
middle_name = models.CharField(
max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
)
# middle_name = models.CharField(
# max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
# )
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
gender = models.CharField(
choices=[("m", _("Male")), ("f", _("Female"))],
@ -1061,6 +1065,10 @@ class Customer(models.Model):
address = models.CharField(
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")
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
@ -1069,24 +1077,85 @@ class Customer(models.Model):
verbose_name_plural = _("Customers")
def __str__(self):
middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name}{middle} {self.last_name}"
# middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name} {self.last_name}"
@property
def get_full_name(self):
return f"{self.first_name} {self.middle_name} {self.last_name}"
def full_name(self):
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})
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})
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
def deactivate_account(self):
self.active = False
self.customer_model.active = False
self.user.is_active = False
self.customer_model.save()
self.user.save()
self.save()
class Organization(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="organizations"
)
customer_model = models.ForeignKey(
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='organization_profile')
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
crn = models.CharField(
max_length=15, verbose_name=_("Commercial Registration Number")
)
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
@ -1094,6 +1163,7 @@ class Organization(models.Model, LocalizedNameMixin):
logo = models.ImageField(
upload_to="logos", blank=True, null=True, verbose_name=_("Logo")
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
@ -1103,7 +1173,61 @@ class Organization(models.Model, LocalizedNameMixin):
def __str__(self):
return self.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.name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
},
)
try:
customer.additional_info.update({"customer_info": customer_dict})
except Exception:
pass
customer.save()
return customer
def update_user_model(self):
user = self.user
user.first_name = self.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.name
customer.address_1 = self.address
customer.phone = self.phone_number
customer.email = self.email
try:
customer.additional_info.update({"customer_info": customer_dict})
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.name,
)
user.set_password("Tenhal@123")
user.save()
return user
def deactivate_account(self):
self.active = False
self.user.is_active = False
self.customer_model.active = False
self.user.save()
self.customer_model.save()
self.save()
class Representative(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(
@ -1471,6 +1595,9 @@ class Vendor(models.Model, LocalizedNameMixin):
vrn = models.CharField(
max_length=15, unique=True, verbose_name=_("VAT Registration Number")
)
vendor_model = models.ForeignKey(
VendorModel, on_delete=models.DO_NOTHING, verbose_name=_("Vendor Model"),null=True,blank=True
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
@ -1491,6 +1618,56 @@ class Vendor(models.Model, LocalizedNameMixin):
def __str__(self):
return self.name
def create_vendor_model(self):
entity = self.dealer.entity
additionals = to_dict(self)
if not self.vendor_model:
vendor = entity.create_vendor(
vendor_model_kwargs={
"vendor_name": self.name,
"vendor_number": self.crn,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
"tax_id_number": self.vrn,
"active": True,
"hidden": False,
"additional_info": additionals,
}
)
self.vendor_model = vendor
self.save()
def update_vendor_model(self):
additionals = to_dict(self)
self.vendor_model.vendor_name = self.name
self.vendor_model.vendor_number = self.crn
self.vendor_model.address_1 = self.address
self.vendor_model.phone = self.phone_number
self.vendor_model.email = self.email
self.vendor_model.tax_id_number = self.vrn
self.vendor_model.additional_info = additionals
self.vendor_model.save()
def create_vendor_account(self,role):
entity = self.dealer.entity
coa = entity.get_default_coa()
last_account = entity.get_all_accounts().filter(role=role).order_by('-created').first()
if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}"
elif len(last_account.code) > 4:
code = f"{int(last_account.code)+1}"
if not entity.get_all_accounts().filter(name=self.name, role=role,coa_model=coa,balance_type="credit",active=True).exists():
entity.create_account(
name=self.name,
code=code,
role=role,
coa_model=coa,
balance_type="credit",
active=True
)
class Payment(models.Model):
METHOD_CHOICES = [

View File

@ -190,74 +190,10 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
:return: None
"""
if created:
entity = EntityModel.objects.filter(admin=instance.dealer.user).first()
additionals = to_dict(instance)
vendor = entity.create_vendor(
vendor_model_kwargs={
"vendor_name": instance.name,
"vendor_number": instance.crn,
"address_1": instance.address,
"phone": instance.phone_number,
"email": instance.email,
"tax_id_number": instance.vrn,
"active": True,
"hidden": False,
"additional_info": additionals,
}
)
coa = entity.get_default_coa()
last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first()
# code = f"{int(last_account.code)}{1:03d}"
if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}"
elif len(last_account.code) > 4:
code = f"{int(last_account.code)+1}"
account = entity.create_account(
name=instance.name,
code=code,
role=roles.LIABILITY_CL_ACC_PAYABLE,
coa_model=coa,
balance_type="credit",
active=True
)
print(f"VendorModel created for Vendor: {instance.name}")
@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)
instance.create_vendor_model()
instance.create_vendor_account(roles.LIABILITY_CL_ACC_PAYABLE)
else:
instance.update_vendor_model()
# Create Item
@receiver(post_save, sender=models.Car)
@ -800,7 +736,13 @@ def update_finance_cost(sender, instance, created, **kwargs):
if created:
entity = instance.car.dealer.entity
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)
save_journal(instance,ledger,vendor)

View File

@ -76,26 +76,26 @@ urlpatterns = [
# CRM URLs
path("customers/", views.CustomerListView.as_view(), name="customer_list"),
path(
"customers/<uuid:pk>/",
"customers/<int:pk>/",
views.CustomerDetailView.as_view(),
name="customer_detail",
),
path(
"customers/create/", views.CustomerCreateView, name="customer_create"
"customers/create/", views.CustomerCreateView.as_view(), name="customer_create"
),
path(
"customers/<uuid:pk>/update/",
views.CustomerUpdateView,
"customers/<int:pk>/update/",
views.CustomerUpdateView.as_view(),
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(
"customers/<str:customer_id>/opportunities/create/",
views.OpportunityCreateView.as_view(),
name="create_opportunity",
),
path(
"customers/<uuid:pk>/add-note/",
"customers/<int:pk>/add-note/",
views.add_note_to_customer,
name="add_note_to_customer",
),
@ -202,15 +202,15 @@ urlpatterns = [
path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'),
# Vendor URLs
path("vendors", views.VendorListView.as_view(), name="vendor_list"),
path("vendors/<uuid:pk>/", views.vendorDetailView, name="vendor_detail"),
path("vendors/<int:pk>/", views.vendorDetailView, name="vendor_detail"),
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
path(
"vendors/<uuid:pk>/update/",
"vendors/<int:pk>/update/",
views.VendorUpdateView.as_view(),
name="vendor_update",
),
path(
"vendors/<uuid:pk>/delete/",
"vendors/<int:pk>/delete/",
views.delete_vendor,
name="vendor_delete",
),
@ -374,22 +374,22 @@ path(
"organizations/", views.OrganizationListView.as_view(), name="organization_list"
),
path(
"organizations/<uuid:pk>/",
"organizations/<int:pk>/",
views.OrganizationDetailView.as_view(),
name="organization_detail",
),
path(
"organizations/create/",
views.OrganizationCreateView,
views.OrganizationCreateView.as_view(),
name="organization_create",
),
path(
"organizations/<uuid:pk>/update/",
views.OrganizationUpdateView,
"organizations/<int:pk>/update/",
views.OrganizationUpdateView.as_view(),
name="organization_update",
),
path(
"organizations/<uuid:pk>/delete/",
"organizations/<int:pk>/delete/",
views.OrganizationDeleteView,
name="organization_delete",
),

View File

@ -388,7 +388,7 @@ def get_financial_values(model):
if i.item_model.additional_info["additional_services"]:
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"]
]
)
@ -1042,6 +1042,7 @@ class CarFinanceCalculator:
total_vat_amount = total_price_discounted * self.vat_rate
return {
"total_price_before_discount": round(total_price, 2), # total_price_before_discount,
"total_price": round(total_price_discounted, 2), # total_price_discounted,
"total_vat_amount": round(total_vat_amount, 2), # total_vat_amount,
"total_discount": round(total_discount,2),
@ -1055,6 +1056,7 @@ class CarFinanceCalculator:
"cars": [self._get_car_data(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_before_discount": totals['total_price_before_discount'],
"total_vat": totals['total_vat_amount'] + totals['total_price'],
"total_vat_amount": totals['total_vat_amount'],
"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

@ -579,7 +579,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields["vendor"].queryset = dealer.entity.get_vendors().filter(active=True)
form.fields["vendor"].queryset = dealer.vendors.all()
return form
def get_success_url(self):
@ -595,6 +595,12 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
messages.success(self.request, _("Car saved successfully"))
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
context["vendor_exists"] = dealer.vendors.exists()
return context
def car_history(request, pk):
"""
@ -1323,7 +1329,7 @@ class CarUpdateView(
form = super().get_form(form_class)
dealer = get_user_type(self.request)
print(dealer.get_vendors())
form.fields["vendor"].queryset = dealer.get_vendors()
form.fields["vendor"].queryset = dealer.vendors.all()
return form
class CarDeleteView(
@ -1878,7 +1884,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
:ivar permission_required: A list of permissions required to access the view.
:type permission_required: list
"""
model = CustomerModel
model = models.Customer
home_label = _("customers")
context_object_name = "customers"
paginate_by = 10
@ -1889,9 +1895,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
customers = dealer.entity.get_customers().filter(
additional_info__type="customer"
)
customers = dealer.customers.filter(active=True)
return apply_search_filters(customers, query)
def get_context_data(self, **kwargs):
@ -1917,7 +1921,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
:ivar permission_required: The list of permissions required to access this view.
:type permission_required: list[str]
"""
model = CustomerModel
model = models.Customer
template_name = "customers/view_customer.html"
context_object_name = "customer"
permission_required = ["django_ledger.view_customermodel"]
@ -1929,9 +1933,9 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
context["customer_notes"] = models.Notes.objects.filter(
object_id=self.object.pk
)
estimates = entity.get_estimates().filter(customer=self.object)
invoices = entity.get_invoices().filter(customer=self.object)
# txs = entity. transactions(customer=self.object)
estimates = entity.get_estimates().filter(customer=self.object.customer_model)
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
total = estimates.count() + invoices.count()
context["estimates"] = estimates
@ -1941,7 +1945,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
@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
note creation is handled by a form, which is validated after submission. If the form
@ -1958,7 +1962,7 @@ def add_note_to_customer(request, customer_id):
POST request, it renders the note form template with context including
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":
form = forms.NoteForm(request.POST)
if form.is_valid():
@ -2012,131 +2016,71 @@ def add_activity_to_customer(request, pk):
)
@login_required
@permission_required("django_ledger.add_customermodel", raise_exception=True)
def CustomerCreateView(request):
class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
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.
# 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.
: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.
# :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.
# """
model = models.Customer
form_class = forms.CustomerForm
permission_required = ["django_ledger.add_customermodel"]
template_name = "customers/customer_form.html"
success_url = reverse_lazy("customer_list")
success_message = "Customer created successfully"
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
user = form.instance.create_user_model()
customer = form.instance.create_customer_model()
form.instance.user = user
form.instance.customer_model = customer
return super().form_valid(form)
class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
form = forms.CustomerForm()
if request.method == "POST":
form = forms.CustomerForm(request.POST)
dealer = get_user_type(request)
# 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.
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()
# :param request: The HTTP request object used to determine the request method,
# access user session details, and provide request data such as POST content.
# Expected to contain the updated customer data if request method is POST.
# :type request: HttpRequest
messages.success(request, _("Customer created successfully"))
return redirect("customer_list")
# :param pk: The primary key of the CustomerModel object that is to be updated.
# :type pk: int
except Exception as e:
messages.error(request, _(f"An error occurred: {str(e)}"))
else:
messages.error(request, _("Please correct the errors below"))
# :return: A rendered HTML template displaying the customer form pre-filled
# with existing data if a GET request is received. On successful form
# submission (POST request), redirects to the customer list page
# and displays a success message. In case of invalid data or errors,
# returns the rendered form template with the validation errors.
# :rtype: HttpResponse
# """
model = models.Customer
form_class = forms.CustomerForm
permission_required = ["django_ledger.change_customermodel"]
template_name = "customers/customer_form.html"
success_url = reverse_lazy("customer_list")
success_message = "Customer updated successfully"
return render(request, "customers/customer_form.html", {"form": form})
@login_required
@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.
:param request: The HTTP request object used to determine the request method,
access user session details, and provide request data such as POST content.
Expected to contain the updated customer data if request method is POST.
:type request: HttpRequest
:param pk: The primary key of the CustomerModel object that is to be updated.
:type pk: int
:return: A rendered HTML template displaying the customer form pre-filled
with existing data if a GET request is received. On successful form
submission (POST request), redirects to the customer list page
and displays a success message. In case of invalid data or errors,
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)
instance.customer_name = customer_name
instance.address_1 = customer_dict["address"]
instance.phone = customer_dict["phone_number"]
instance.email = customer_dict["email"]
customer_dict["pk"] = str(instance.pk)
instance.additional_info.update({"customer_info": customer_dict})
try:
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})
def form_valid(self, form):
form.instance.update_user_model()
form.instance.update_customer_model()
return super().form_valid(form)
@login_required
@ -2156,14 +2100,9 @@ def delete_customer(request, pk):
:return: A redirect response to the customer list page.
:rtype: HttpResponseRedirect
"""
customer = get_object_or_404(models.CustomerModel, pk=pk)
user = User.objects.get(email=customer.email)
customer.active = False
user.is_active = False
customer.save()
user.save()
messages.success(request, _("Customer deleted successfully"))
customer = get_object_or_404(models.Customer, pk=pk)
customer.deactivate_account()
messages.success(request, _("Customer deactivated successfully"))
return redirect("customer_list")
@ -2193,17 +2132,17 @@ class VendorListView(LoginRequiredMixin, ListView):
ordered by their creation date in descending order.
:type ordering: list
"""
model = VendorModel
model = models.Vendor
context_object_name = "vendors"
paginate_by = 10
template_name = "vendors/vendors_list.html"
ordering = ["-created"]
# ordering = ["-created"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
vendors = dealer.entity.get_vendors().filter(active=True)
return apply_search_filters(vendors, query)
# def get_queryset(self):
# query = self.request.GET.get("q")
# dealer = get_user_type(self.request)
# vendors = dealer.entity.get_vendors().filter(active=True)
# return apply_search_filters(vendors, query)
@login_required
@ -2295,24 +2234,24 @@ class VendorUpdateView(
success_url = reverse_lazy("vendor_list")
success_message = _("Vendor updated successfully")
def get_initial(self):
initial = super().get_initial()
initial = self.object.additional_info
return initial
# def get_initial(self):
# initial = super().get_initial()
# initial = self.object.additional_info
# return initial
def form_valid(self, form):
instance = form.save(commit=False)
instance.vendor_name = self.request.POST["name"]
instance.vendor_number = self.request.POST["crn"]
instance.address_1 = self.request.POST["address"]
instance.phone = self.request.POST["phone_number"]
instance.email = self.request.POST["email"]
instance.tax_id_number = self.request.POST["vrn"]
additionals = form.cleaned_data
additionals["phone_number"] = str(additionals["phone_number"])
instance.additional_info = additionals
instance.save()
# instance = form.save(commit=False)
print(self.request.POST)
# instance.vendor_name = self.request.POST["name"]
# instance.vendor_number = self.request.POST["crn"]
# instance.address_1 = self.request.POST["address"]
# instance.phone = self.request.POST["phone_number"]
# instance.email = self.request.POST["email"]
# instance.tax_id_number = self.request.POST["vrn"]
# additionals = form.cleaned_data
# additionals["phone_number"] = str(additionals["phone_number"])
# instance.additional_info = additionals
# instance.save()
return super().form_valid(form)
@ -2333,8 +2272,9 @@ def delete_vendor(request, pk):
:rtype: HttpResponseRedirect
"""
vendor = get_object_or_404(models.Vendor, pk=pk)
# vendor.active = False
vendor.delete()
vendor.active = False
vendor.vendor_model.active = False
vendor.save()
messages.success(request, _("Vendor deleted successfully"))
return redirect("vendor_list")
@ -2794,7 +2734,7 @@ class OrganizationListView(LoginRequiredMixin, ListView):
:ivar paginate_by: The number of organizations displayed per page.
:type paginate_by: int
"""
model = CustomerModel
model = models.Organization
template_name = "organizations/organization_list.html"
context_object_name = "organizations"
paginate_by = 10
@ -2802,9 +2742,7 @@ class OrganizationListView(LoginRequiredMixin, ListView):
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
organization = dealer.entity.get_customers().filter(
additional_info__type="organization", active=True
)
organization = dealer.organizations.filter(active=True)
return apply_search_filters(organization, query)
@ -2827,131 +2765,72 @@ class OrganizationDetailView(LoginRequiredMixin, DetailView):
template for accessing the organization's data.
:type context_object_name: str
"""
model = CustomerModel
model = models.Organization
template_name = "organizations/organization_detail.html"
context_object_name = "organization"
@login_required
def OrganizationCreateView(request):
class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Handles the creation of a new organization via a web form. This view allows the
authenticated user to submit data for creating an organization. If a POST request
is received, it validates the data, checks for duplicate organizations, and
creates a customer linked to the organization, including its associated
information such as address, phone number, and logo. Upon success, the user
is redirected to the organization list, and a success message is displayed.
# Handles the creation of a new organization via a web form. This view allows the
# authenticated user to submit data for creating an organization. If a POST request
# is received, it validates the data, checks for duplicate organizations, and
# creates a customer linked to the organization, including its associated
# 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.
:type request: HttpRequest
:return: An HTTP response object rendering the organization create form page or
redirecting the user after a successful creation.
:rtype: HttpResponse
# :param request: The HTTP request object containing data for creating an organization.
# :type request: HttpRequest
# :return: An HTTP response object rendering the organization create form page or
# redirecting the user after a successful creation.
# :rtype: HttpResponse
# """
model = models.Organization
form_class = forms.OrganizationForm
permission_required = ["django_ledger.add_customermodel"]
template_name = "organizations/organization_form.html"
success_url = reverse_lazy("organization_list")
success_message = "Organization created successfully"
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
user = form.instance.create_user_model()
customer = form.instance.create_customer_model()
form.instance.user = user
form.instance.customer_model = customer
return super().form_valid(form)
class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
if request.method == "POST":
form = forms.OrganizationForm(request.POST)
if CustomerModel.objects.filter(email=request.POST["email"]).exists():
messages.error(
request, _("An organization with this email already exists.")
)
return redirect("organization_create")
# Handles the update of an organization instance. This view fetches the organization
# based on the provided primary key (pk) and renders a form for editing the
# organization attributes. When a POST request is made, this view validates and
# processes the form data, updates the organization instance, and saves the changes.
# If the request method is not POST, it initializes the form with existing organization
# data for rendering.
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)
# :param request: The HTTP request object. Must be authenticated via login.
# :type request: HttpRequest
# :param pk: The primary key of the organization to be updated.
# :type pk: int
# :return: An HTTP response object. Either renders the organization form or redirects
# to the organization list upon successful update.
# :rtype: HttpResponse
# """
model = models.Organization
form_class = forms.OrganizationForm
permission_required = ["django_ledger.change_customermodel"]
template_name = "organizations/organization_form.html"
success_url = reverse_lazy("organization_list")
success_message = "Organization updated successfully"
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})
def form_valid(self, form):
form.instance.update_user_model()
form.instance.update_customer_model()
return super().form_valid(form)
@login_required
def OrganizationUpdateView(request, pk):
"""
Handles the update of an organization instance. This view fetches the organization
based on the provided primary key (pk) and renders a form for editing the
organization attributes. When a POST request is made, this view validates and
processes the form data, updates the organization instance, and saves the changes.
If the request method is not POST, it initializes the form with existing organization
data for rendering.
:param request: The HTTP request object. Must be authenticated via login.
:type request: HttpRequest
:param pk: The primary key of the organization to be updated.
:type pk: int
:return: An HTTP response object. Either renders the organization form or redirects
to the organization list upon successful update.
:rtype: HttpResponse
"""
organization = get_object_or_404(CustomerModel, pk=pk)
if request.method == "POST":
form = forms.OrganizationForm(request.POST)
organization_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
dealer = get_user_type(request)
instance = dealer.entity.get_customers().get(
pk=organization.additional_info["customer_info"]["pk"]
)
name = organization_dict["first_name"] + " " + organization_dict["last_name"]
instance.customer_name = name
instance.address_1 = organization_dict["address"]
instance.phone = organization_dict["phone_number"]
instance.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
else:
organization_dict["logo"] = organization.additional_info["customer_info"][
"logo"
]
organization_dict["pk"] = str(instance.pk)
instance.additional_info["customer_info"] = organization_dict
instance.additional_info["type"] = "organization"
instance.save()
messages.success(request, _("Organization created successfully"))
return redirect("organization_list")
else:
form = forms.OrganizationForm(
initial=organization.additional_info["customer_info"] or {}
)
# form.fields.pop("logo", None)
return render(request, "organizations/organization_form.html", {"form": form})
# class OrganizationDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
# model = models.Organization
# template_name = "organizations/organization_confirm_delete.html"
# success_url = reverse_lazy("organization_list")
# success_message = "Organization deleted successfully."
@login_required
def OrganizationDeleteView(request, pk):
"""
@ -2967,14 +2846,9 @@ def OrganizationDeleteView(request, pk):
:return: An HTTP response redirecting to the organization list view.
:rtype: HttpResponseRedirect
"""
organization = get_object_or_404(CustomerModel, pk=pk)
try:
User.objects.get(email=organization.email).delete()
organization.delete()
messages.success(request, _("Organization deleted successfully"))
except Exception as e:
print("unable to delete user", e)
messages.error(request, _("Unable to delete organization"))
organization = get_object_or_404(models.Organization, pk=pk)
organization.deactivate_account()
messages.success(request, _("Organization Deactivated successfully"))
return redirect("organization_list")
@ -3633,8 +3507,9 @@ def create_estimate(request, pk=None):
data = json.loads(request.body)
title = data.get("title")
customer_id = data.get("customer")
terms = data.get("terms")
customer = entity.get_customers().filter(pk=customer_id).first()
# terms = data.get("terms")
# customer = entity.get_customers().filter(pk=customer_id).first()
customer = models.Customer.objects.filter(pk=customer_id).first()
items = data.get("item", [])
quantities = data.get("quantity", [])
@ -3673,7 +3548,7 @@ def create_estimate(request, pk=None):
{"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")},
)
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):
item_quantity_map = {}
@ -3767,12 +3642,11 @@ def create_estimate(request, pk=None):
}
)
form = forms.EstimateModelCreateForm(
entity_slug=entity.slug, user_model=entity.admin
)
form.fields["customer"].queryset = entity.get_customers().filter(
active=True, additional_info__type="customer"
)
# form = forms.EstimateModelCreateForm(
# entity_slug=entity.slug, user_model=entity.admin
# )
form = forms.EstimateModelCreateForm()
form.fields["customer"].queryset = dealer.customers.all()
if pk:
opportunity = models.Opportunity.objects.get(pk=pk)
@ -3982,13 +3856,9 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie
dealer = get_user_type(self.request)
estimate = kwargs.get("object")
if estimate.get_itemtxs_data():
data = get_financial_values(estimate)
kwargs["vat_amount"] = data["vat_amount"]
kwargs["total"] = data["grand_total"]
kwargs["discount_amount"] = data["discount_amount"]
kwargs["vat"] = data["vat"]
kwargs["additional_services"] = data["additional_services"]
# data = get_financial_values(estimate)
calculator = CarFinanceCalculator(estimate)
kwargs["data"] = calculator.get_finance_data()
kwargs["dealer"] = dealer
return super().get_context_data(**kwargs)
@ -6215,11 +6085,12 @@ def send_email_view(request, pk):
{dealer.phone_number}
هيكل | Haikal
"""
subject = _("Quotation")
# subject = _("Quotation")
send_email(
str(settings.DEFAULT_FROM_EMAIL),
estimate.customer.email,
subject,
"عرض سعر - Quotation",
msg,
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
static/images/logos/vendors/image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -22,7 +22,7 @@
</div>
<div class="row mb-3">
<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 %}
{{ form|crispy }}
<div class="col-12">

View File

@ -70,17 +70,17 @@
</td>
<td class="name align-middle white-space-nowrap ps-0">
<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>
</div>
</div>
</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="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="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.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">
{{ 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">
{% if customer.active %}
<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="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="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 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>
</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>
</div>
<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">
<h5 class="text-body-secondary">{% trans 'Email' %}</h5><a href="{{ customer.email}}">{{ customer.email }}</a>
</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>

View File

@ -20,28 +20,28 @@
{% if not car.ready %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("This car information is not complete , please add colors and finances before making it ready for sale .") }}</p>
<p class="mb-0 flex-1">{{ _("This car information is not complete , please add colors and finances before making it ready for sale .") }}</p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if car.get_transfer.status == "draft" %}
<div class="alert alert-outline-info d-flex align-items-center" role="alert">
<div class="alert alert-outline-info d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("Action Required , Please Approved The Tranfer Request Of This Car .") }}</p>
<p class="mb-0 flex-1">{{ _("Action Required , Please Approved The Tranfer Request Of This Car .") }}</p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if car.get_transfer and car.get_transfer.status == "approved" %}
<div class="alert alert-outline-info d-flex align-items-center" role="alert">
<div class="alert alert-outline-info d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("Car Is In Transfer Process To Another Dealer, Please Wait For The Acceptance .") }}</p>
<p class="mb-0 flex-1">{{ _("Car Is In Transfer Process To Another Dealer, Please Wait For The Acceptance .") }}</p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if car.is_reserved %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("This car is reserved until ") }}{{ car.get_reservation.reserved_until }}</p>
<p class="mb-0 flex-1">{{ _("This car is reserved until ") }}{{ car.get_reservation.reserved_until }}</p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
@ -98,7 +98,7 @@
{% if car.vendor %}
<tr>
<th>{% trans "Vendor"|capfirst %}</th>
<td>{{ car.vendor.vendor_name }}</td>
<td>{{ car.vendor.name }}</td>
</tr>
{% endif %}
<tr>
@ -361,7 +361,7 @@
<p class="card-header rounded-top fw-bold">{% trans 'Transfer Details' %}</p>
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
<table class="table table-sm fs-9 mb-0 overflow-hidden">
<table class="table table-sm fs-9 mb-0 overflow-hidden">
<thead>
<tr>
<th>{% trans "Action" %}</th>
@ -373,7 +373,7 @@
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge badge-phoenix badge-phoenix-info">Transfer</span></td>
<td>
@ -396,11 +396,11 @@
<a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}">Approve</a>
{% endif %}
</td>
<td>
<td>
<a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}?action=cancel">Cancel</a>
</td>
</tr>
</tbody>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -6,14 +6,24 @@
height: auto;
margin: 0 auto;
}
.disabled{
opacity: 0.5;
pointer-events: none;
}
</style>
<!-- JavaScript Section -->
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
<div class=" container-fluid m-0">
{% if not vendor_exists %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("Please Add A Vendor, Before Adding A Car .") }}</p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class=" container-fluid m-0 {% if not vendor_exists %}disabled{% endif %}">
<form method="post" id="carForm" class="form needs-validation" novalidate>
{% csrf_token %}
{% include 'partials/form_errors.html' %}

View File

@ -5,10 +5,10 @@
<div class="row my-4">
<h2>{{ organization.get_local_name }}</h2>
<ul class="list-group mb-4">
<li class="list-group-item"><strong>{% trans "CRN" %}:</strong> {{ organization.additional_info.customer_info.crn }}</li>
<li class="list-group-item"><strong>{% trans "VRN" %}:</strong> {{ organization.additional_info.customer_info.vrn }}</li>
<li class="list-group-item"><strong>{% trans "Phone" %}:</strong> {{ organization.additional_info.customer_info.phone_number }}</li>
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.additional_info.customer_info.address }}</li>
<li class="list-group-item"><strong>{% trans "CRN" %}:</strong> {{ organization.crn }}</li>
<li class="list-group-item"><strong>{% trans "VRN" %}:</strong> {{ organization.vrn }}</li>
<li class="list-group-item"><strong>{% trans "Phone" %}:</strong> {{ organization.phone_number }}</li>
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.address }}</li>
</ul>
<div class="d-flex">
<a href="{% url 'organization_update' organization.pk %}" class="btn btn-sm btn-warning me-2">{% trans "Edit" %}</a>

View File

@ -102,26 +102,26 @@
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center">
<div>
<a class="fs-8 fw-bold" href="{% url 'organization_detail' org.pk %}">{{ org.customer_name }}</a>
<a class="fs-8 fw-bold" href="{% url 'organization_detail' org.pk %}">{{ org.name }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p><span class="badge badge-phoenix badge-phoenix-primary">{{ org.customer_name }}</span>
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p><span class="badge badge-phoenix badge-phoenix-primary">{{ org.name }}</span>
</div>
</div>
</div>
</td>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">{{ org.additional_info.customer_info.crn }}</td>
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">{{ org.additional_info.customer_info.vrn }}</td>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">{{ org.crn }}</td>
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">{{ org.vrn }}</td>
<td class="phone align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">
<a class="text-body-highlight" href="tel:{{ org.phone }}">{{ org.phone }}</a>
<a class="text-body-highlight" href="tel:{{ org.phone }}">{{ org.phone_number }}</a>
</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">{{ org.address_1 }}</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">{{ org.address }}</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ org.created|date }}</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.django_ledger.change_customermodel %}
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" 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>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'organization_update' org.pk %}" class="dropdown-item text-success-dark">{% trans 'Edit' %}</a>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'organization_update' org.pk %}" class="dropdown-item text-success-dark">{% trans 'Edit' %}</a>
{% if perms.django_ledger.delete_customermodel %}
<div class="dropdown-divider"></div><button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans 'Delete' %}</button>
{% endif %}

View File

@ -13,7 +13,7 @@
</style>
{% endblock customCSS %}
{% block content %}
<div class="row mt-4">
<div class="row mt-4">
{% if not items %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
@ -32,7 +32,7 @@
<h3 class="text-center"><i class="fa-regular fa-file-lines"></i> {% trans "Create Quotation" %}</h3>
{% csrf_token %}
<div class="row g-3 col-10">
{{ form|crispy }}
{{ form|crispy }}
<div class="row mt-5">
<div id="formrow">
<h3 class="text-start"><i class="fa-solid fa-car-side"></i> {{ _("Cars") }}</h3>
@ -70,7 +70,7 @@
{% endblock content %}
{% block customJS %}
<script>
<script>
const Toast = Swal.mixin({
toast: true,
position: "top-end",
@ -98,7 +98,7 @@
</div>
<div class="mb-2 col-sm-2">
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
</div>
</div>
<div class="mb-2 col-sm-1">
<button class="btn btn-danger removeBtn"><i class="fa-solid fa-trash"></i> {{ _("Remove") }}</button>
</div>
@ -133,7 +133,6 @@
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
title: document.querySelector('[name=title]').value,
customer: document.querySelector('[name=customer]').value,
terms: document.querySelector('[name=terms]').value,
item: [],
quantity: [],
opportunity_id: "{{opportunity_id}}"
@ -148,7 +147,7 @@
formData.quantity.push(input.value);
});
console.log(formData);
try {
// Send data to the server using fetch
const response = await fetch("{% url 'estimate_create' %}", {
@ -175,8 +174,8 @@
notify("error","Unexpected response from the server");
}
} catch (error) {
notify("error", error);
notify("error", error);
}
});
</script>

View File

@ -99,23 +99,25 @@
<td></td>
<td>
<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>
</td>
</tr>
<tr>
<td><strong>{{ dealer.name }}</strong></td>
<td></td>
<td class="ps-1"><strong>Customer Name</strong></td>
<td class="text-center">{{ dealer.arabic_name }}<br>{{ dealer.name }}</td>
<td class="text-end"><strong>{{ dealer.arabic_name }}</strong></td>
</tr>
<tr>
<td><strong>Address</strong></td>
<td>{{ dealer.address }}</td>
<td class="ps-1"><strong>Address</strong></td>
<td class="text-center">{{ dealer.address }}</td>
<td class="text-end"> <strong>العنوان</strong></td>
</tr>
<tr>
<td><strong>Phone</strong></td>
<td>{{ dealer.phone_number }}</td>
<td class="ps-1"><strong>Phone</strong></td>
<td class="text-center">{{ dealer.phone_number }}</td>
<td class="text-end"><strong>جوال</strong></td>
</tr>
<tr>
@ -190,8 +192,8 @@
<tr>
<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.total_price|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.total_price_before_discount|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.grand_total|floatformat:2 }}</td>
</tr>
</tbody>
</table>

View File

@ -96,46 +96,40 @@
<h5 class="fs-5">Tax&nbsp;Invoice&nbsp;&nbsp;/&nbsp;&nbsp;فاتورة&nbsp;ضريبية</h5>
</div>
<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>
<td>
<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>
</td>
<td></td>
<td>
<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>
</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>
<td class="ps-1"><strong>Customer Name</strong></td>
<td class="text-center">{{ dealer.arabic_name }}<br>{{ dealer.name }}</td>
<td class="text-end"><strong>اسم&nbsp;العميل</strong></td>
</tr>
<tr>
<td class="ps-1"><strong>Address</strong></td>
<td class="text-center">{{ dealer.address }}</td>
<td class="text-end"> <strong>العنوان</strong></td>
</tr>
<tr>
<td class="ps-1"><strong>Phone</strong></td>
<td class="text-center">{{ dealer.phone_number }}</td>
<td class="text-end"><strong>جوال</strong></td>
</tr>
<tr>
<td class="ps-1"><strong>VAT Number</strong></td>
<td class="text-center">{{ dealer.vrn }}</td>
<td class="text-end p-1"><strong>الرقم الضريبي</strong></td>
</tr>
</table>
<table class="table table-sm table-bordered border-gray-50">
@ -203,8 +197,8 @@
<tr>
<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.total_price|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.total_price_before_discount|floatformat }}</td>
<td class="text-center fs-10 align-content-center">{{ data.grand_total|floatformat }}</td>
</tr>
</tbody>
</table>

View File

@ -27,7 +27,7 @@
<div class="row">
<div class="col-xl-9">
<form class="row g-3 mb-9" method="post" class="form" novalidate>
<form class="row g-3 mb-9" method="post" class="form" enctype="multipart/form-data" novalidate >
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}

View File

@ -114,7 +114,7 @@
<div class="avatar avatar-xl me-3"><img class="rounded-circle" src="{% static 'images/icons/picture.svg' %}" alt="" />
{% endif %}
</div>
<div><a class="fs-8 fw-bold" href="">{{ vendor.vendor_name }}</a>
<div><a class="fs-8 fw-bold" href="">{{ vendor.pk }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2">{{ vendor.vendor_name }}</p><span class="badge badge-phoenix badge-phoenix-primary">{{ vendor.id}}</span>
</div>
@ -122,11 +122,11 @@
</div>
</td>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ vendor.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:{{ vendor.phone }}">{{ vendor.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:{{ vendor.phone }}">{{ vendor.phone_number }}</a></td>
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">{{ vendor.contact_person }}</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">
{{ vendor.address_1 }}</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ vendor.created|date }}</td>
{{ vendor.address }}</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ vendor.created_at|date }}</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" 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>