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 from inventory import models as inventory_models
class LoginView(APIView): class LoginView(APIView):
permission_classes = [permissions.AllowAny,] permission_classes = [permissions.AllowAny,]

View File

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

View File

@ -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 datetime
import django.core.validators import django.core.validators
@ -17,17 +17,11 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
# ('appointment', '0002_alter_workinghours_options'), ('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'), ('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.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 = [ operations = [
@ -92,7 +86,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('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')), ('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')), ('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')), ('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
('taxable', models.BooleanField(default=False, verbose_name='taxable')), ('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')), ('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={ options={
'verbose_name': 'Additional Services', 'verbose_name': 'Additional Services',
@ -140,7 +134,7 @@ class Migration(migrations.Migration):
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')), ('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')), ('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')), ('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')), ('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={ options={
@ -322,7 +316,7 @@ class Migration(migrations.Migration):
('logo', models.ImageField(blank=True, null=True, upload_to='logos/users', verbose_name='Logo')), ('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')), ('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated 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)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
@ -339,7 +333,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)), ('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')), ('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=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('additional_info', models.JSONField(blank=True, default=dict, null=True)), ('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_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=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='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=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='django_ledger.accountmodel')),
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')), ('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_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=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='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=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='django_ledger.accountmodel')),
], ],
), ),
migrations.CreateModel( 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')), ('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')), ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), ('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')), ('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_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')), ('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', name='SaleOrder',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('comments', models.TextField(blank=True, null=True)),
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)), ('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
('created', models.DateTimeField(auto_now_add=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')), ('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=settings.DJANGO_LEDGER_INVOICE_MODEL, verbose_name='Invoice')), ('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={ options={
'ordering': ['-created'], 'ordering': ['-created'],
@ -599,15 +593,15 @@ class Migration(migrations.Migration):
name='Schedule', name='Schedule',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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_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))), ('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)), ('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)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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')), ('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)), ('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')), ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('closed', models.BooleanField(default=False, verbose_name='Closed')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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')), ('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={ options={
'verbose_name': 'Vendor', '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)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'verbose_name': 'Payment History',
'verbose_name_plural': 'Payment Histories', 'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'], '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')], '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.utils.timezone import now
from django.contrib.auth.models import Group 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 .mixins import LocalizedNameMixin
from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel,AccountModel,EntityManagementModel from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel,AccountModel,EntityManagementModel
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
@ -376,7 +376,7 @@ class Car(models.Model):
) )
vendor = models.ForeignKey( vendor = models.ForeignKey(
VendorModel, "Vendor",
models.DO_NOTHING, models.DO_NOTHING,
related_name="cars", related_name="cars",
verbose_name=_("Vendor"), verbose_name=_("Vendor"),
@ -1032,18 +1032,22 @@ class Priority(models.TextChoices):
HIGH = "high", _("High") HIGH = "high", _("High")
class Customer(models.Model): class Customer(models.Model):
dealer = models.ForeignKey( dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="customers" Dealer, on_delete=models.CASCADE, related_name="customers"
) )
customer_model = models.ForeignKey(
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
title = models.CharField( title = models.CharField(
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
) )
first_name = models.CharField(max_length=50, verbose_name=_("First Name")) first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
middle_name = models.CharField( # middle_name = models.CharField(
max_length=50, blank=True, null=True, verbose_name=_("Middle Name") # max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
) # )
last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
gender = models.CharField( gender = models.CharField(
choices=[("m", _("Male")), ("f", _("Female"))], choices=[("m", _("Male")), ("f", _("Female"))],
@ -1061,6 +1065,10 @@ class Customer(models.Model):
address = models.CharField( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField(
upload_to="customers/", blank=True, null=True, verbose_name=_("Image")
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
@ -1069,24 +1077,85 @@ class Customer(models.Model):
verbose_name_plural = _("Customers") verbose_name_plural = _("Customers")
def __str__(self): def __str__(self):
middle = f" {self.middle_name}" if self.middle_name else "" # middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name}{middle} {self.last_name}" return f"{self.first_name} {self.last_name}"
@property @property
def get_full_name(self): def full_name(self):
return f"{self.first_name} {self.middle_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
def create_customer_model(self):
customer_dict = to_dict(self)
customer = self.dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": self.full_name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
},
)
try:
customer.additional_info.update({"customer_info": customer_dict})
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): class Organization(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey( dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="organizations" 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")) name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
crn = models.CharField( crn = models.CharField(
max_length=15, verbose_name=_("Commercial Registration Number") max_length=15, verbose_name=_("Commercial Registration Number")
) )
vrn = models.CharField(max_length=15, verbose_name=_("VAT 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")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
@ -1094,6 +1163,7 @@ class Organization(models.Model, LocalizedNameMixin):
logo = models.ImageField( logo = models.ImageField(
upload_to="logos", blank=True, null=True, verbose_name=_("Logo") 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")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
@ -1103,7 +1173,61 @@ class Organization(models.Model, LocalizedNameMixin):
def __str__(self): def __str__(self):
return self.name 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): class Representative(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey( dealer = models.ForeignKey(
@ -1471,6 +1595,9 @@ class Vendor(models.Model, LocalizedNameMixin):
vrn = models.CharField( vrn = models.CharField(
max_length=15, unique=True, verbose_name=_("VAT Registration Number") 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")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name")) name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
@ -1491,6 +1618,56 @@ class Vendor(models.Model, LocalizedNameMixin):
def __str__(self): def __str__(self):
return self.name 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): class Payment(models.Model):
METHOD_CHOICES = [ METHOD_CHOICES = [

View File

@ -190,74 +190,10 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
:return: None :return: None
""" """
if created: if created:
entity = EntityModel.objects.filter(admin=instance.dealer.user).first() instance.create_vendor_model()
additionals = to_dict(instance) instance.create_vendor_account(roles.LIABILITY_CL_ACC_PAYABLE)
vendor = entity.create_vendor( else:
vendor_model_kwargs={ instance.update_vendor_model()
"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)
# Create Item # Create Item
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
@ -800,7 +736,13 @@ def update_finance_cost(sender, instance, created, **kwargs):
if created: if created:
entity = instance.car.dealer.entity entity = instance.car.dealer.entity
vendor = instance.car.vendor vendor = instance.car.vendor
name = f"{instance.car.vin}-{instance.car.id_car_make.name}-{instance.car.id_car_model.name}-{instance.car.year}-{vendor.vendor_name}" vin = instance.car.vin if instance.car.vin else ""
make = instance.car.id_car_make.name if instance.car.id_car_make else ""
model = instance.car.id_car_model.name if instance.car.id_car_model else ""
year = instance.car.year
vendor_name = vendor.name if vendor else ""
name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity) ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity)
save_journal(instance,ledger,vendor) save_journal(instance,ledger,vendor)

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

View File

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

View File

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

View File

@ -20,28 +20,28 @@
{% if not car.ready %} {% if not car.ready %}
<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> <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> <button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endif %} {% endif %}
{% if car.get_transfer.status == "draft" %} {% 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> <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> <button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endif %} {% endif %}
{% if car.get_transfer and car.get_transfer.status == "approved" %} {% 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> <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> <button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endif %} {% endif %}
{% if car.is_reserved %} {% 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> <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> <button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endif %} {% endif %}
@ -98,7 +98,7 @@
{% if car.vendor %} {% if car.vendor %}
<tr> <tr>
<th>{% trans "Vendor"|capfirst %}</th> <th>{% trans "Vendor"|capfirst %}</th>
<td>{{ car.vendor.vendor_name }}</td> <td>{{ car.vendor.name }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
@ -361,7 +361,7 @@
<p class="card-header rounded-top fw-bold">{% trans 'Transfer Details' %}</p> <p class="card-header rounded-top fw-bold">{% trans 'Transfer Details' %}</p>
<div class="card-body"> <div class="card-body">
<div class="table-responsive scrollbar mb-3"> <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> <thead>
<tr> <tr>
<th>{% trans "Action" %}</th> <th>{% trans "Action" %}</th>
@ -373,7 +373,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><span class="badge badge-phoenix badge-phoenix-info">Transfer</span></td> <td><span class="badge badge-phoenix badge-phoenix-info">Transfer</span></td>
<td> <td>
@ -396,11 +396,11 @@
<a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}">Approve</a> <a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}">Approve</a>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}?action=cancel">Cancel</a> <a class="btn btn-sm btn-phoenix-success" href="{% url 'transfer_detail' car.get_transfer.pk %}?action=cancel">Cancel</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@ -6,14 +6,24 @@
height: auto; height: auto;
margin: 0 auto; margin: 0 auto;
} }
.disabled{
opacity: 0.5;
pointer-events: none;
}
</style> </style>
<!-- JavaScript Section --> <!-- JavaScript Section -->
<script src="{% static 'vendors/zxing/index.min.js' %}"></script> <script src="{% static 'vendors/zxing/index.min.js' %}"></script>
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script> <script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
{% if not vendor_exists %}
<div class=" container-fluid m-0"> <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> <form method="post" id="carForm" class="form needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
{% include 'partials/form_errors.html' %} {% include 'partials/form_errors.html' %}

View File

@ -5,10 +5,10 @@
<div class="row my-4"> <div class="row my-4">
<h2>{{ organization.get_local_name }}</h2> <h2>{{ organization.get_local_name }}</h2>
<ul class="list-group mb-4"> <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 "CRN" %}:</strong> {{ organization.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 "VRN" %}:</strong> {{ organization.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 "Phone" %}:</strong> {{ organization.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 "Address" %}:</strong> {{ organization.address }}</li>
</ul> </ul>
<div class="d-flex"> <div class="d-flex">
<a href="{% url 'organization_update' organization.pk %}" class="btn btn-sm btn-warning me-2">{% trans "Edit" %}</a> <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"> <td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <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"> <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> </div>
</div> </div>
</td> </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="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.additional_info.customer_info.vrn }}</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"> <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>
<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="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"> <td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.django_ledger.change_customermodel %} {% if perms.django_ledger.change_customermodel %}
<div class="btn-reveal-trigger position-static"> <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> <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"> <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> <a href="{% url 'organization_update' org.pk %}" class="dropdown-item text-success-dark">{% trans 'Edit' %}</a>
{% if perms.django_ledger.delete_customermodel %} {% 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> <div class="dropdown-divider"></div><button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans 'Delete' %}</button>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@
<div class="row"> <div class="row">
<div class="col-xl-9"> <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 %} {% csrf_token %}
{{ redirect_field }} {{ redirect_field }}
{{ form|crispy }} {{ 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="" /> <div class="avatar avatar-xl me-3"><img class="rounded-circle" src="{% static 'images/icons/picture.svg' %}" alt="" />
{% endif %} {% endif %}
</div> </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"> <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> <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> </div>
@ -122,11 +122,11 @@
</div> </div>
</td> </td>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ vendor.email }}</a></td> <td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ 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="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"> <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> {{ vendor.address }}</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ vendor.created|date }}</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"> <td class="align-middle white-space-nowrap text-end pe-0 ps-4">
<div class="btn-reveal-trigger position-static"> <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> <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>