diff --git a/inventory/forms.py b/inventory/forms.py index 019e7b92..2db9a9ed 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -10,6 +10,8 @@ from django.core.validators import MinLengthValidator from django import forms from plans.models import PlanPricing from django.contrib.auth import get_user_model + +from inventory.validators import SaudiPhoneNumberValidator from .models import CustomGroup, Status, Stage from .mixins import AddClassMixin from django_ledger.forms.invoice import ( @@ -46,6 +48,7 @@ from .models import ( CarModel, SaleOrder, CarMake, + Customer, DealerSettings ) from django_ledger import models as ledger_models @@ -58,6 +61,14 @@ import django_tables2 as tables User = get_user_model() + +class SaudiPhoneNumberField(forms.CharField): + def __init__(self, *args, **kwargs): + kwargs.setdefault('min_length', 10) + kwargs.setdefault('max_length', 13) + super().__init__(*args, **kwargs) + self.validators.append(SaudiPhoneNumberValidator()) + class AdditionalServiceForm(forms.ModelForm): """ A form used for creating and updating instances of the @@ -104,20 +115,14 @@ class StaffForm(forms.ModelForm): widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), queryset=Service.objects.all(), required=False,) - phone_number = forms.CharField( + phone_number = SaudiPhoneNumberField( required=False, - max_length=10, - min_length=10, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': _('Phone Number'), 'id': 'phone' }), label=_('Phone Number'), - validators=[RegexValidator( - regex=r'^05[0-9]{8}$', - message=_('Enter a valid phone number (8-15 digits, starting with 05)') - )] ) class Meta: model = Staff @@ -147,7 +152,9 @@ class DealerForm(forms.ModelForm): :type address: str :ivar logo: Logo of the dealer. :type logo: File + """ + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) class Meta: model = Dealer fields = [ @@ -161,61 +168,78 @@ class DealerForm(forms.ModelForm): ] -class CustomerForm(forms.Form): - """ - Represents a form for collecting customer information. +class CustomerForm(forms.ModelForm): + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) - This form is used to gather and validate customer details such as name, - email, phone number, national ID, tax registration details, and address. - It includes several fields with validation constraints and specific - requirements. It is designed to handle both required and optional fields, - ensuring the correctness of user inputs. + class Meta: + model = Customer + fields = [ + 'title', + "first_name", + "last_name", + "email", + "phone_number", + "national_id", + "dob", + "address", + 'image', + ] +# class CustomerForm(forms.Form): +# """ +# Represents a form for collecting customer information. - :ivar first_name: Customer's first name. - :type first_name: forms.CharField - :ivar last_name: Customer's last name. - :type last_name: forms.CharField - :ivar arabic_name: Customer's name in Arabic. - :type arabic_name: forms.CharField - :ivar email: Customer's email address. - :type email: forms.EmailField - :ivar phone_number: Customer's phone number. Validates the format and - ensures the number is in the Saudi Arabia region. - :type phone_number: PhoneNumberField - :ivar national_id: Customer's national ID. Optional field limited to - a maximum length of 10 characters. - :type national_id: forms.CharField - :ivar crn: Commercial registration number (CRN) of the customer. Optional field. - :type crn: forms.CharField - :ivar vrn: Value-added tax registration number (VRN) of the customer. - Optional field. - :type vrn: forms.CharField - :ivar address: Customer's address. - :type address: forms.CharField - """ - first_name = forms.CharField() - last_name = forms.CharField() - arabic_name = forms.CharField() - email = forms.EmailField() - # phone_number = PhoneNumberField( - # label=_("Phone Number"), - # widget=forms.TextInput( - # attrs={ - # "placeholder": _("Phone"), - # } - # ), - # region="SA", - # error_messages={ - # "required": _("This field is required."), - # "invalid": _("Phone number must be in the format 05xxxxxxxx"), - # }, - # required=True, - # ) - phone_number = forms.CharField(label=_("Phone Number"),min_length=10,max_length=10,validators=[RegexValidator(regex='^05[0-9]{8}$')], required=True) - national_id = forms.CharField(max_length=10,required=False) - crn = forms.CharField(required=False) - vrn = forms.CharField(required=False) - address = forms.CharField() +# This form is used to gather and validate customer details such as name, +# email, phone number, national ID, tax registration details, and address. +# It includes several fields with validation constraints and specific +# requirements. It is designed to handle both required and optional fields, +# ensuring the correctness of user inputs. + +# :ivar first_name: Customer's first name. +# :type first_name: forms.CharField +# :ivar last_name: Customer's last name. +# :type last_name: forms.CharField +# :ivar arabic_name: Customer's name in Arabic. +# :type arabic_name: forms.CharField +# :ivar email: Customer's email address. +# :type email: forms.EmailField +# :ivar phone_number: Customer's phone number. Validates the format and +# ensures the number is in the Saudi Arabia region. +# :type phone_number: PhoneNumberField +# :ivar national_id: Customer's national ID. Optional field limited to +# a maximum length of 10 characters. +# :type national_id: forms.CharField +# :ivar crn: Commercial registration number (CRN) of the customer. Optional field. +# :type crn: forms.CharField +# :ivar vrn: Value-added tax registration number (VRN) of the customer. +# Optional field. +# :type vrn: forms.CharField +# :ivar address: Customer's address. +# :type address: forms.CharField +# """ +# first_name = forms.CharField() +# last_name = forms.CharField() +# arabic_name = forms.CharField() +# email = forms.EmailField() +# # phone_number = PhoneNumberField( +# # label=_("Phone Number"), +# # widget=forms.TextInput( +# # attrs={ +# # "placeholder": _("Phone"), +# # } +# # ), +# # region="SA", +# # error_messages={ +# # "required": _("This field is required."), +# # "invalid": _("Phone number must be in the format 05xxxxxxxx"), +# # }, +# # required=True, +# # ) +# phone_number = forms.CharField(label=_("Phone Number"),min_length=10,max_length=10,validators=[RegexValidator(regex='^05[0-9]{8}$')], required=True) +# national_id = forms.CharField(max_length=10,required=False) +# crn = forms.CharField(required=False) +# vrn = forms.CharField(required=False) +# address = forms.CharField() +# image = forms.ImageField(required=False) class OrganizationForm(CustomerForm): @@ -479,18 +503,7 @@ class VendorForm(forms.ModelForm): :ivar Meta: Inner class to define metadata for the Vendor form. :type Meta: Type[VendorForm.Meta] """ - phone_number = forms.CharField( - label=_("Phone Number"), - min_length=10, - max_length=13, - validators=[ - RegexValidator( - regex=r'^(\+9665|05)[0-9]{8}$', - message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)") - ) - ], - required=True -) + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) contact_person = forms.CharField(label=_("Contact Person")) class Meta: @@ -577,6 +590,8 @@ class RepresentativeForm(forms.ModelForm): :ivar Meta.fields: The fields from the model to include in the form. :type Meta.fields: list of str """ + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) + class Meta: model = Representative fields = [ @@ -795,21 +810,7 @@ class WizardForm2(forms.Form): # }, # required=True, # ) - phone_number = forms.CharField( - required=False, - max_length=10, - min_length=10, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': _('Phone Number'), - 'id': 'phone' - }), - label=_('Phone Number'), - validators=[RegexValidator( - regex=r'^05[0-9]{8}$', - message=_('Enter a valid phone number (10 digits, starting with 05)') - )] - ) + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) class WizardForm3(forms.Form): @@ -1006,6 +1007,8 @@ class LeadForm(forms.ModelForm): options are displayed until a car make is selected. :type id_car_model: ModelChoiceField """ + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) + id_car_make = forms.ModelChoiceField( label=_("Make"), queryset=CarMake.objects.filter(is_sa_import=True), @@ -1213,71 +1216,75 @@ class SaleOrderForm(forms.ModelForm): } -class EstimateModelCreateForm(EstimateModelCreateFormBase): - """ - Defines the EstimateModelCreateForm class, which is used to create and manage - forms for EstimateModel. This form handles the rendering and validation - of specific fields, their input widgets, and labels. +class EstimateModelCreateForm(forms.Form): + title = forms.CharField(max_length=255) + customer = forms.ModelChoiceField(queryset=Customer.objects.none()) - The purpose of this class is to provide a structured way to handle - EstimateModel instances and their related fields, ensuring a user-friendly - form interface with proper field configuration. It facilitates fetching - related data, such as customer queries, based on user-specific parameters. +# class EstimateModelCreateForm(EstimateModelCreateFormBase): +# """ +# Defines the EstimateModelCreateForm class, which is used to create and manage +# forms for EstimateModel. This form handles the rendering and validation +# of specific fields, their input widgets, and labels. - :ivar ENTITY_SLUG: A string that represents the entity context in which the - form operates. - :type ENTITY_SLUG: str - :ivar USER_MODEL: The user model that provides methods and objects needed - to filter and query customers. - :type USER_MODEL: Any - :ivar fields: A dictionary representing fields included in the form such as - "title", "customer", and "terms". - :type fields: dict - :ivar widgets: A dictionary defining custom input widgets for form fields, - enabling specific attributes like classes and identifiers for styling or - functionality purposes. - :type widgets: dict - :ivar labels: A dictionary specifying the human-readable labels for form fields. - :type labels: dict - """ - class Meta: - model = ledger_models.EstimateModel - fields = ["title","customer", "terms"] - widgets = { - "customer": forms.Select( - attrs={ - "id": "djl-customer-estimate-customer-input", - "class": "input", - "label": _("Customer"), - } - ), - 'terms': forms.Select(attrs={ - 'id': 'djl-customer-estimate-terms-input', - 'class': 'input', - 'label': _('Terms'), - }), - 'title': forms.TextInput(attrs={ - 'id': 'djl-customer-job-title-input', - 'class': 'input' + ' is-large', - 'label': _('Title'), - }) - } - labels = { - 'title': _('Title'), - 'terms': _('Terms'), - "customer": _("Customer"), - } +# The purpose of this class is to provide a structured way to handle +# EstimateModel instances and their related fields, ensuring a user-friendly +# form interface with proper field configuration. It facilitates fetching +# related data, such as customer queries, based on user-specific parameters. - def __init__(self, *args, entity_slug, user_model, **kwargs): - super(EstimateModelCreateForm, self).__init__( - *args, entity_slug=entity_slug, user_model=user_model, **kwargs - ) - self.ENTITY_SLUG = entity_slug - self.USER_MODEL = user_model - self.fields["customer"].queryset = self.get_customer_queryset() +# :ivar ENTITY_SLUG: A string that represents the entity context in which the +# form operates. +# :type ENTITY_SLUG: str +# :ivar USER_MODEL: The user model that provides methods and objects needed +# to filter and query customers. +# :type USER_MODEL: Any +# :ivar fields: A dictionary representing fields included in the form such as +# "title", "customer", and "terms". +# :type fields: dict +# :ivar widgets: A dictionary defining custom input widgets for form fields, +# enabling specific attributes like classes and identifiers for styling or +# functionality purposes. +# :type widgets: dict +# :ivar labels: A dictionary specifying the human-readable labels for form fields. +# :type labels: dict +# """ +# class Meta: +# model = ledger_models.EstimateModel +# fields = ["title","customer", "terms"] +# widgets = { +# "customer": forms.Select( +# attrs={ +# "id": "djl-customer-estimate-customer-input", +# "class": "input", +# "label": _("Customer"), +# } +# ), +# 'terms': forms.Select(attrs={ +# 'id': 'djl-customer-estimate-terms-input', +# 'class': 'input', +# 'label': _('Terms'), +# }), +# 'title': forms.TextInput(attrs={ +# 'id': 'djl-customer-job-title-input', +# 'class': 'input' + ' is-large', +# 'label': _('Title'), +# }) +# } +# labels = { +# 'title': _('Title'), +# 'terms': _('Terms'), +# "customer": _("Customer"), +# } - def get_customer_queryset(self): - return self.USER_MODEL.dealer.entity.get_customers() +# def __init__(self, *args, entity_slug, user_model, **kwargs): +# super(EstimateModelCreateForm, self).__init__( +# *args, entity_slug=entity_slug, user_model=user_model, **kwargs +# ) +# self.ENTITY_SLUG = entity_slug +# self.USER_MODEL = user_model +# self.fields["customer"].queryset = self.get_customer_queryset() + +# def get_customer_queryset(self): +# return self.USER_MODEL.dealer.entity.get_customers() class OpportunityStatusForm(forms.Form): @@ -1589,21 +1596,7 @@ class PaymentPlanForm(forms.Form): label=_('Email Address') ) - phone = forms.CharField( - required=False, - max_length=10, - min_length=10, - widget=forms.TextInput(attrs={ - 'class': 'form-control', - 'placeholder': _('Phone Number'), - 'id': 'phone' - }), - label=_('Phone Number'), - validators=[RegexValidator( - regex=r'^05[0-9]{8}$', - message=_('Enter a valid phone number (10 digits, starting with 05)') - )] - ) + phone_number = SaudiPhoneNumberField(label=_('Phone Number')) # Credit Card Fields (not saved to database) card_number = CreditCardField( diff --git a/inventory/migrations/0004_customer_image.py b/inventory/migrations/0004_customer_image.py new file mode 100644 index 00000000..1edce473 --- /dev/null +++ b/inventory/migrations/0004_customer_image.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0005_customer_customer_model.py b/inventory/migrations/0005_customer_customer_model.py new file mode 100644 index 00000000..5eb7c725 --- /dev/null +++ b/inventory/migrations/0005_customer_customer_model.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0006_remove_customer_middle_name_customer_active.py b/inventory/migrations/0006_remove_customer_middle_name_customer_active.py new file mode 100644 index 00000000..67754c52 --- /dev/null +++ b/inventory/migrations/0006_remove_customer_middle_name_customer_active.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0007_customer_customer_type.py b/inventory/migrations/0007_customer_customer_type.py new file mode 100644 index 00000000..9cfd37f3 --- /dev/null +++ b/inventory/migrations/0007_customer_customer_type.py @@ -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'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 8087eb5f..0ca1af6a 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1032,18 +1032,22 @@ class Priority(models.TextChoices): HIGH = "high", _("High") + class Customer(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="customers" ) + customer_model = models.ForeignKey( + CustomerModel, on_delete=models.SET_NULL, null=True + ) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile') title = models.CharField( choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") ) first_name = models.CharField(max_length=50, verbose_name=_("First Name")) - middle_name = models.CharField( - max_length=50, blank=True, null=True, verbose_name=_("Middle Name") - ) + # middle_name = models.CharField( + # max_length=50, blank=True, null=True, verbose_name=_("Middle Name") + # ) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) gender = models.CharField( choices=[("m", _("Male")), ("f", _("Female"))], @@ -1061,6 +1065,14 @@ class Customer(models.Model): address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) + active = models.BooleanField(default=True, verbose_name=_("Active")) + image = models.ImageField( + upload_to="customers/", blank=True, null=True, verbose_name=_("Image") + ) + customer_type = models.CharField( + choices=[("customer", _("Customer")),("organization", _("Organization"))], default="customer", max_length=15, verbose_name=_("Customer Type"), + null=True,blank=True + ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) @@ -1069,13 +1081,64 @@ class Customer(models.Model): verbose_name_plural = _("Customers") def __str__(self): - middle = f" {self.middle_name}" if self.middle_name else "" - return f"{self.first_name}{middle} {self.last_name}" + # middle = f" {self.middle_name}" if self.middle_name else "" + return f"{self.first_name} {self.last_name}" @property - def get_full_name(self): - return f"{self.first_name} {self.middle_name} {self.last_name}" + def full_name(self): + return f"{self.first_name} {self.last_name}" + def create_customer_model(self): + customer_dict = to_dict(self) + customer = self.dealer.entity.create_customer( + commit=False, + customer_model_kwargs={ + "customer_name": self.full_name, + "address_1": self.address, + "phone": self.phone_number, + "email": self.email, + }, + ) + try: + customer.additional_info.update({"customer_info": customer_dict}) + customer.additional_info.update({"type": "customer"}) + except Exception: + pass + customer.save() + return customer + + def update_user_model(self): + user = self.user + user.first_name = self.first_name + user.last_name = self.last_name + user.email = self.email + user.save() + return user + def update_customer_model(self): + customer_dict = to_dict(self) + customer = self.customer_model + customer.customer_name = self.full_name + customer.address_1 = self.address + customer.phone = self.phone_number + customer.email = self.email + try: + customer.additional_info.update({"customer_info": customer_dict}) + customer.additional_info.update({"type": "customer"}) + except Exception: + pass + customer.save() + return customer + + def create_user_model(self): + user = User.objects.create_user( + username=self.email, + email=self.email, + first_name=self.first_name, + last_name=self.last_name, + ) + user.set_password("Tenhal@123") + user.save() + return user class Organization(models.Model, LocalizedNameMixin): dealer = models.ForeignKey( diff --git a/inventory/signals.py b/inventory/signals.py index 81a38e04..c8225da2 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -195,39 +195,6 @@ def create_ledger_vendor(sender, instance, created, **kwargs): else: instance.update_vendor_model() -@receiver(post_save, sender=models.CustomerModel) -def create_customer_user(sender, instance, created, **kwargs): - """ - Connects to the `post_save` signal of the `CustomerModel` to create a user object - associated with the customer instance whenever a new `CustomerModel` instance is - created. Retrieves customer-specific information from `additional_info` to initialize - and configure the associated user object. Ensures the user object created has - unusable passwords by default. - - :param sender: The model class that sends the signal (`CustomerModel`). - :param instance: The instance of `CustomerModel` that triggered the signal. - :param created: A boolean indicating whether a new instance was created. - :param kwargs: Additional keyword arguments passed by the signal. - :return: None - """ - if created: - try: - first_name = instance.additional_info.get("customer_info").get("first_name") - last_name = instance.additional_info.get("customer_info").get("last_name") - user = User.objects.create( - username=instance.email, - email=instance.email, - first_name=first_name if first_name else '', - last_name=last_name if last_name else '', - ) - instance.additional_info.update({"user_info": to_dict(user)}) - user.set_unusable_password() - user.save() - instance.user = user - instance.save() - except Exception as e: - print(e) - # Create Item @receiver(post_save, sender=models.Car) def create_item_model(sender, instance, created, **kwargs): @@ -769,7 +736,13 @@ def update_finance_cost(sender, instance, created, **kwargs): if created: entity = instance.car.dealer.entity vendor = instance.car.vendor - name = f"{instance.car.vin}-{instance.car.id_car_make.name}-{instance.car.id_car_model.name}-{instance.car.year}-{vendor.vendor_name}" + vin = instance.car.vin if instance.car.vin else "" + make = instance.car.id_car_make.name if instance.car.id_car_make else "" + model = instance.car.id_car_model.name if instance.car.id_car_model else "" + year = instance.car.year + vendor_name = vendor.name if vendor else "" + + name = f"{vin}-{make}-{model}-{year}-{vendor_name}" ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity) save_journal(instance,ledger,vendor) diff --git a/inventory/urls.py b/inventory/urls.py index 14e29625..da103742 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -76,26 +76,26 @@ urlpatterns = [ # CRM URLs path("customers/", views.CustomerListView.as_view(), name="customer_list"), path( - "customers//", + "customers//", views.CustomerDetailView.as_view(), name="customer_detail", ), path( - "customers/create/", views.CustomerCreateView, name="customer_create" + "customers/create/", views.CustomerCreateView.as_view(), name="customer_create" ), path( - "customers//update/", - views.CustomerUpdateView, + "customers//update/", + views.CustomerUpdateView.as_view(), name="customer_update", ), - path("customers//delete/", views.delete_customer, name="customer_delete"), + path("customers//delete/", views.delete_customer, name="customer_delete"), path( "customers//opportunities/create/", views.OpportunityCreateView.as_view(), name="create_opportunity", ), path( - "customers//add-note/", + "customers//add-note/", views.add_note_to_customer, name="add_note_to_customer", ), @@ -380,7 +380,7 @@ path( ), path( "organizations/create/", - views.OrganizationCreateView, + views.OrganizationCreateView.as_view(), name="organization_create", ), path( diff --git a/inventory/utils.py b/inventory/utils.py index aa71f067..55104f09 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -388,7 +388,7 @@ def get_financial_values(model): if i.item_model.additional_info["additional_services"]: additional_services.extend( [ - {"name": x.name, "price": x.price} + {"name": x['name'], "price": x["price"]} for x in i.item_model.additional_info["additional_services"] ] ) @@ -1042,6 +1042,7 @@ class CarFinanceCalculator: total_vat_amount = total_price_discounted * self.vat_rate return { + "total_price_before_discount": round(total_price, 2), # total_price_before_discount, "total_price": round(total_price_discounted, 2), # total_price_discounted, "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, "total_discount": round(total_discount,2), @@ -1055,6 +1056,7 @@ class CarFinanceCalculator: "cars": [self._get_car_data(item) for item in self.item_transactions], "quantity": sum(self._get_quantity(item) for item in self.item_transactions), "total_price": totals['total_price'], + "total_price_before_discount": totals['total_price_before_discount'], "total_vat": totals['total_vat_amount'] + totals['total_price'], "total_vat_amount": totals['total_vat_amount'], "total_discount": totals['total_discount'], diff --git a/inventory/validators.py b/inventory/validators.py index e69de29b..e1d20bb3 100644 --- a/inventory/validators.py +++ b/inventory/validators.py @@ -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)") + ) \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index ed76dc28..0e82dc1b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1884,7 +1884,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: A list of permissions required to access the view. :type permission_required: list """ - model = CustomerModel + model = models.Customer home_label = _("customers") context_object_name = "customers" paginate_by = 10 @@ -1895,9 +1895,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_queryset(self): query = self.request.GET.get("q") dealer = get_user_type(self.request) - customers = dealer.entity.get_customers().filter( - additional_info__type="customer" - ) + customers = dealer.customers.all() return apply_search_filters(customers, query) def get_context_data(self, **kwargs): @@ -1923,7 +1921,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView :ivar permission_required: The list of permissions required to access this view. :type permission_required: list[str] """ - model = CustomerModel + model = models.Customer template_name = "customers/view_customer.html" context_object_name = "customer" permission_required = ["django_ledger.view_customermodel"] @@ -1935,9 +1933,9 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView context["customer_notes"] = models.Notes.objects.filter( object_id=self.object.pk ) - estimates = entity.get_estimates().filter(customer=self.object) - invoices = entity.get_invoices().filter(customer=self.object) - # txs = entity. transactions(customer=self.object) + estimates = entity.get_estimates().filter(customer=self.object.customer_model) + invoices = entity.get_invoices().filter(customer=self.object.customer_model) + total = estimates.count() + invoices.count() context["estimates"] = estimates @@ -1947,7 +1945,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView @login_required -def add_note_to_customer(request, customer_id): +def add_note_to_customer(request, pk): """ This function allows authenticated users to add a note to a specific customer. The note creation is handled by a form, which is validated after submission. If the form @@ -1964,7 +1962,7 @@ def add_note_to_customer(request, customer_id): POST request, it renders the note form template with context including the form and customer. """ - customer = get_object_or_404(CustomerModel, pk=customer_id) + customer = get_object_or_404(models.Customer, pk=pk) if request.method == "POST": form = forms.NoteForm(request.POST) if form.is_valid(): @@ -2018,131 +2016,163 @@ def add_activity_to_customer(request, pk): ) -@login_required -@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 - and request methods are utilized. It provides feedback to the user about the success or failure of - the customer creation process. When the form is submitted and valid, it checks for duplicate - customers based on the email provided before proceeding with the customer creation. +class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): + 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" - :param request: The HTTP request object containing metadata about the request initiated by the user. - :type request: HttpRequest - :return: The rendered form page or a redirect to the customer list page upon successful creation. - :rtype: HttpResponse - :raises PermissionDenied: If the user does not have the required permissions to access the view. - """ - form = forms.CustomerForm() - if request.method == "POST": - form = forms.CustomerForm(request.POST) - dealer = get_user_type(request) + 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() - if form.is_valid(): - if ( - dealer.entity.get_customers() - .filter(email=form.cleaned_data["email"]) - .exists() - ): - messages.error(request, _("Customer with this email already exists")) - else: - customer_name = ( - f"{form.cleaned_data['first_name']} " - f"{form.cleaned_data['last_name']}" - ) - customer_dict = { - x: request.POST[x] - for x in request.POST - if x != "csrfmiddlewaretoken" - } - try: - customer = dealer.entity.create_customer( - commit=False, - customer_model_kwargs={ - "customer_name": customer_name, - "address_1": form.cleaned_data["address"], - "phone": form.cleaned_data["phone_number"], - "email": form.cleaned_data["email"], - }, - ) - customer.additional_info.update({"customer_info": customer_dict}) - customer.additional_info.update({"type": "customer"}) - customer.save() + form.instance.user = user + form.instance.customer_model = customer - messages.success(request, _("Customer created successfully")) - return redirect("customer_list") + return super().form_valid(form) - except Exception as e: - messages.error(request, _(f"An error occurred: {str(e)}")) - else: - messages.error(request, _("Please correct the errors below")) +# @login_required +# @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 +# and request methods are utilized. It provides feedback to the user about the success or failure of +# the customer creation process. When the form is submitted and valid, it checks for duplicate +# customers based on the email provided before proceeding with the customer creation. - return render(request, "customers/customer_form.html", {"form": form}) +# :param request: The HTTP request object containing metadata about the request initiated by the user. +# :type request: HttpRequest +# :return: The rendered form page or a redirect to the customer list page upon successful creation. +# :rtype: HttpResponse +# :raises PermissionDenied: If the user does not have the required permissions to access the view. +# """ +# form = forms.CustomerForm() +# if request.method == "POST": +# form = forms.CustomerForm(request.POST) +# dealer = get_user_type(request) + +# if form.is_valid(): +# if ( +# dealer.entity.get_customers() +# .filter(email=form.cleaned_data["email"]) +# .exists() +# ): +# messages.error(request, _("Customer with this email already exists")) +# else: +# customer_name = ( +# f"{form.cleaned_data['first_name']} " +# f"{form.cleaned_data['last_name']}" +# ) +# customer_dict = { +# x: request.POST[x] +# for x in request.POST +# if x != "csrfmiddlewaretoken" +# } +# try: +# customer = dealer.entity.create_customer( +# commit=False, +# customer_model_kwargs={ +# "customer_name": customer_name, +# "address_1": form.cleaned_data["address"], +# "phone": form.cleaned_data["phone_number"], +# "email": form.cleaned_data["email"], +# }, +# ) +# customer.additional_info.update({"customer_info": customer_dict}) +# customer.additional_info.update({"type": "customer"}) +# customer.save() + +# messages.success(request, _("Customer created successfully")) +# return redirect("customer_list") + +# except Exception as e: +# messages.error(request, _(f"An error occurred: {str(e)}")) +# else: +# messages.error(request, _("Please correct the errors below")) + +# return render(request, "customers/customer_form.html", {"form": form}) -@login_required -@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. +class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): + 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" - :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 + def form_valid(self, form): + form.instance.update_user_model() + form.instance.update_customer_model() + return super().form_valid(form) - :param pk: The primary key of the CustomerModel object that is to be updated. - :type pk: int +# @login_required +# @permission_required("django_ledger.change_customermodel", raise_exception=True) +# def CustomerUpdateView(request, pk): +# """ +# Updates the details of an existing customer in the database. This view is +# accessible only to logged-in users with the appropriate permissions. It +# handles both GET (form rendering with pre-filled customer data) and POST +# (submitting updates) requests. Data validation and customer updates are +# conducted based on the received form data. - :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"] +# :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 - 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"] +# :param pk: The primary key of the CustomerModel object that is to be updated. +# :type pk: int - 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) +# :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.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}) +# 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 @@ -2162,9 +2192,11 @@ def delete_customer(request, pk): :return: A redirect response to the customer list page. :rtype: HttpResponseRedirect """ - customer = get_object_or_404(models.CustomerModel, pk=pk) - user = User.objects.get(email=customer.email) + customer = get_object_or_404(models.Customer, pk=pk) + customer_model = customer.customer_model + user = customer.user customer.active = False + customer_model.active = False user.is_active = False customer.save() user.save() @@ -2838,59 +2870,77 @@ class OrganizationDetailView(LoginRequiredMixin, DetailView): context_object_name = "organization" -@login_required -def OrganizationCreateView(request): - """ - Handles the creation of a new organization via a web form. This view allows the - authenticated user to submit data for creating an organization. If a POST request - is received, it validates the data, checks for duplicate organizations, and - creates a customer linked to the organization, including its associated - information such as address, phone number, and logo. Upon success, the user - is redirected to the organization list, and a success message is displayed. +class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): + model = models.Organization + form_class = forms.OrganizationForm + permission_required = ["django_ledger.add_customermodel"] + template_name = "customers/organization_form.html" + success_url = reverse_lazy("organization_list") + success_message = "Customer created successfully" - :param request: The HTTP request object containing data for creating an organization. - :type request: HttpRequest - :return: An HTTP response object rendering the organization create form page or - redirecting the user after a successful creation. - :rtype: HttpResponse - """ - if request.method == "POST": - form = forms.OrganizationForm(request.POST) - if CustomerModel.objects.filter(email=request.POST["email"]).exists(): - messages.error( - request, _("An organization with this email already exists.") - ) - return redirect("organization_create") + 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.customer_type = 'organization' + form.instance.user = user + form.instance.customer_model = customer - organization_dict = { - x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken" - } - dealer = get_user_type(request) - name = organization_dict["first_name"] + " " + organization_dict["last_name"] - customer = dealer.entity.create_customer( - commit=False, - customer_model_kwargs={ - "customer_name": name, - "address_1": organization_dict["address"], - "phone": organization_dict["phone_number"], - "email": organization_dict["email"], - }, - ) - image = request.FILES.get("logo") - if image: - file_name = default_storage.save("images/{}".format(image.name), image) - file_url = default_storage.url(file_name) + return super().form_valid(form) +# @login_required +# def OrganizationCreateView(request): +# """ +# Handles the creation of a new organization via a web form. This view allows the +# authenticated user to submit data for creating an organization. If a POST request +# is received, it validates the data, checks for duplicate organizations, and +# creates a customer linked to the organization, including its associated +# information such as address, phone number, and logo. Upon success, the user +# is redirected to the organization list, and a success message is displayed. - organization_dict["logo"] = file_url - organization_dict["pk"] = str(customer.pk) - customer.additional_info.update({"customer_info": organization_dict}) - customer.additional_info.update({"type": "organization"}) - customer.save() - messages.success(request, _("Organization created successfully")) - return redirect("organization_list") - else: - form = forms.OrganizationForm() - return render(request, "organizations/organization_form.html", {"form": form}) +# :param request: The HTTP request object containing data for creating an organization. +# :type request: HttpRequest +# :return: An HTTP response object rendering the organization create form page or +# redirecting the user after a successful creation. +# :rtype: HttpResponse +# """ +# if request.method == "POST": +# form = forms.OrganizationForm(request.POST) +# if CustomerModel.objects.filter(email=request.POST["email"]).exists(): +# messages.error( +# request, _("An organization with this email already exists.") +# ) +# return redirect("organization_create") + +# organization_dict = { +# x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken" +# } +# dealer = get_user_type(request) +# name = organization_dict["first_name"] + " " + organization_dict["last_name"] +# customer = dealer.entity.create_customer( +# commit=False, +# customer_model_kwargs={ +# "customer_name": name, +# "address_1": organization_dict["address"], +# "phone": organization_dict["phone_number"], +# "email": organization_dict["email"], +# }, +# ) +# image = request.FILES.get("logo") +# if image: +# file_name = default_storage.save("images/{}".format(image.name), image) +# file_url = default_storage.url(file_name) + +# organization_dict["logo"] = file_url +# organization_dict["pk"] = str(customer.pk) +# customer.additional_info.update({"customer_info": organization_dict}) +# customer.additional_info.update({"type": "organization"}) +# customer.save() +# messages.success(request, _("Organization created successfully")) +# return redirect("organization_list") +# else: +# form = forms.OrganizationForm() +# return render(request, "organizations/organization_form.html", {"form": form}) @login_required @@ -3639,8 +3689,9 @@ def create_estimate(request, pk=None): data = json.loads(request.body) title = data.get("title") customer_id = data.get("customer") - terms = data.get("terms") - customer = entity.get_customers().filter(pk=customer_id).first() + # terms = data.get("terms") + # customer = entity.get_customers().filter(pk=customer_id).first() + customer = models.Customer.objects.filter(pk=customer_id).first() items = data.get("item", []) quantities = data.get("quantity", []) @@ -3679,7 +3730,7 @@ def create_estimate(request, pk=None): {"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")}, ) estimate = entity.create_estimate( - estimate_title=title, customer_model=customer, contract_terms=terms + estimate_title=title, customer_model=customer.customer_model, contract_terms="fixed" ) if isinstance(items, list): item_quantity_map = {} @@ -3773,12 +3824,11 @@ def create_estimate(request, pk=None): } ) - form = forms.EstimateModelCreateForm( - entity_slug=entity.slug, user_model=entity.admin - ) - form.fields["customer"].queryset = entity.get_customers().filter( - active=True, additional_info__type="customer" - ) + # form = forms.EstimateModelCreateForm( + # entity_slug=entity.slug, user_model=entity.admin + # ) + form = forms.EstimateModelCreateForm() + form.fields["customer"].queryset = dealer.customers.all() if pk: opportunity = models.Opportunity.objects.get(pk=pk) @@ -3988,13 +4038,9 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie dealer = get_user_type(self.request) estimate = kwargs.get("object") if estimate.get_itemtxs_data(): - data = get_financial_values(estimate) - - kwargs["vat_amount"] = data["vat_amount"] - kwargs["total"] = data["grand_total"] - kwargs["discount_amount"] = data["discount_amount"] - kwargs["vat"] = data["vat"] - kwargs["additional_services"] = data["additional_services"] + # data = get_financial_values(estimate) + calculator = CarFinanceCalculator(estimate) + kwargs["data"] = calculator.get_finance_data() kwargs["dealer"] = dealer return super().get_context_data(**kwargs) @@ -6221,11 +6267,12 @@ def send_email_view(request, pk): {dealer.phone_number} هيكل | Haikal """ - subject = _("Quotation") + # subject = _("Quotation") + send_email( str(settings.DEFAULT_FROM_EMAIL), estimate.customer.email, - subject, + "عرض سعر - Quotation", msg, ) diff --git a/static/images/customers/image.png b/static/images/customers/image.png new file mode 100644 index 00000000..fb612b2b Binary files /dev/null and b/static/images/customers/image.png differ diff --git a/templates/customers/customer_form.html b/templates/customers/customer_form.html index dbcfc3b9..7aec17b2 100644 --- a/templates/customers/customer_form.html +++ b/templates/customers/customer_form.html @@ -22,7 +22,7 @@
-
+ {% csrf_token %} {{ form|crispy }}
diff --git a/templates/customers/customer_list.html b/templates/customers/customer_list.html index 54fab4b8..dd919e2e 100644 --- a/templates/customers/customer_list.html +++ b/templates/customers/customer_list.html @@ -70,17 +70,17 @@
- {{ customer.email }} - {{ customer.phone }} - {{ customer.additional_info.customer_info.national_id }} + {{ customer.phone_number }} + {{ customer.national_id }} - {{ customer.address_1 }} + {{ customer.address }} {% if customer.active %} {{customer.active}} diff --git a/templates/customers/view_customer.html b/templates/customers/view_customer.html index a86c133d..d44857fc 100644 --- a/templates/customers/view_customer.html +++ b/templates/customers/view_customer.html @@ -41,10 +41,10 @@
-
+
-

{{ customer.customer_name }}

+

{{ customer.full_name }}

{{ customer.created|timesince}}

@@ -69,11 +69,11 @@
{{ _("Address") }}
-

{{ customer.address_1}}

+

{{ customer.address}}

{% trans 'Email' %}
{{ customer.email }}
-
{% trans 'Phone Number' %}
{{ customer.phone }} +
{% trans 'Phone Number' %}
{{ customer.phone_number }}
diff --git a/templates/sales/estimates/estimate_form.html b/templates/sales/estimates/estimate_form.html index ea09aa84..f4eb7279 100644 --- a/templates/sales/estimates/estimate_form.html +++ b/templates/sales/estimates/estimate_form.html @@ -13,7 +13,7 @@ {% endblock customCSS %} {% block content %} -
+
{% if not items %}