diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index b9d44430..b1ddaf0a 100644 Binary files a/inventory/__pycache__/forms.cpython-311.pyc and b/inventory/__pycache__/forms.cpython-311.pyc differ diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index 429d055a..0c155c75 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/services.cpython-311.pyc b/inventory/__pycache__/services.cpython-311.pyc index 7451acb6..c1979932 100644 Binary files a/inventory/__pycache__/services.cpython-311.pyc and b/inventory/__pycache__/services.cpython-311.pyc differ diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index 88448a5c..0329d79c 100644 Binary files a/inventory/__pycache__/views.cpython-311.pyc and b/inventory/__pycache__/views.cpython-311.pyc differ diff --git a/inventory/forms.py b/inventory/forms.py index 1d9083b4..f2160e43 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -110,7 +110,7 @@ class CustomerForm(forms.ModelForm, AddClassMixin): "dob", "email", "national_id", - "city", + "phone_number", "address", ] @@ -583,20 +583,14 @@ class EmailForm(forms.Form): class LeadForm(forms.ModelForm): class Meta: model = Lead - fields = ['title', - 'first_name', - 'last_name', - 'email', - 'phone_number', + fields = ['customer', 'city', - 'salary', - 'obligations', 'id_car_make', 'id_car_model', 'year', 'source', 'channel', - 'assigned', + 'staff', 'priority', ] @@ -624,7 +618,7 @@ class ActivityForm(forms.ModelForm): class OpportunityForm(forms.ModelForm): class Meta: model = Opportunity - fields = ['customer', 'car', 'stage', 'probability', 'closing_date'] + fields = ['customer', 'car', 'stage', 'probability', 'staff', 'closing_date'] class InvoiceModelCreateForm(InvoiceModelCreateFormBase): diff --git a/inventory/migrations/0004_rename_assigned_lead_staff_remove_customer_city_and_more.py b/inventory/migrations/0004_rename_assigned_lead_staff_remove_customer_city_and_more.py new file mode 100644 index 00000000..74725067 --- /dev/null +++ b/inventory/migrations/0004_rename_assigned_lead_staff_remove_customer_city_and_more.py @@ -0,0 +1,90 @@ +# Generated by Django 5.1.4 on 2025-01-17 00:20 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0003_alter_carmake_car_type'), + ] + + operations = [ + migrations.RenameField( + model_name='lead', + old_name='assigned', + new_name='staff', + ), + migrations.RemoveField( + model_name='customer', + name='city', + ), + migrations.RemoveField( + model_name='customer', + name='lead', + ), + migrations.RemoveField( + model_name='customer', + name='staff', + ), + migrations.RemoveField( + model_name='lead', + name='address', + ), + migrations.RemoveField( + model_name='lead', + name='email', + ), + migrations.RemoveField( + model_name='lead', + name='first_name', + ), + migrations.RemoveField( + model_name='lead', + name='last_name', + ), + migrations.RemoveField( + model_name='lead', + name='obligations', + ), + migrations.RemoveField( + model_name='lead', + name='phone_number', + ), + migrations.RemoveField( + model_name='lead', + name='salary', + ), + migrations.RemoveField( + model_name='lead', + name='title', + ), + migrations.RemoveField( + model_name='organization', + name='created_at', + ), + migrations.AddField( + model_name='lead', + name='customer', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.customer'), + preserve_default=False, + ), + migrations.AddField( + model_name='organization', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), + preserve_default=False, + ), + migrations.AddField( + model_name='organization', + name='updated', + field=models.DateTimeField(auto_now=True, verbose_name='Updated'), + ), + migrations.AlterField( + model_name='representative', + name='id_number', + field=models.CharField(max_length=10, unique=True, verbose_name='ID Number'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 39f2f263..62efab07 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -780,23 +780,82 @@ class Priority(models.TextChoices): MEDIUM = "medium", _("Medium") HIGH = "high", _("High") + +class Customer(models.Model): + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE,related_name="customers") + 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")) + last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) + gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender")) + dob = models.DateField(verbose_name=_("Date of Birth")) + email = models.EmailField(unique=True, verbose_name=_("Email")) + national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID")) + phone_number = PhoneNumberField(region="SA", unique=True, verbose_name=_("Phone Number")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) + updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + + class Meta: + verbose_name = _("Customer") + 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}" + + @property + def get_full_name(self): + return f"{self.first_name} {self.middle_name} {self.last_name}" + + +class Organization(models.Model, LocalizedNameMixin): + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations') + name = models.CharField(max_length=255, verbose_name=_("Name")) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + crn = models.CharField(max_length=15, verbose_name=_("Commercial Registration Number")) + vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) + phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + logo = models.ImageField(upload_to="logos", blank=True, null=True, verbose_name=_("Logo")) + created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) + updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + + class Meta: + verbose_name = _("Organization") + verbose_name_plural = _("Organizations") + def __str__(self): + return self.name + + +class Representative(models.Model, LocalizedNameMixin): + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='representatives') + name = models.CharField(max_length=255, verbose_name=_("Name")) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + id_number = models.CharField(max_length=10, unique=True, verbose_name=_("ID Number")) + phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) + email = models.EmailField(max_length=255, verbose_name=_("Email Address")) + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + organization = models.ManyToManyField(Organization, related_name='representatives') + + class Meta: + verbose_name = _("Representative") + verbose_name_plural = _("Representatives") + + def __str__(self): + return self.name + + class Lead(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads") - title = models.CharField(max_length=20, choices=Title.choices, verbose_name=_("Title")) - first_name = models.CharField(max_length=50, verbose_name=_("First Name")) - last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) - email = models.EmailField(unique=True, verbose_name=_("Email"), db_index=True) - phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) - salary = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Salary")) - obligations = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Obligations")) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="leads") id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Make")) id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Model")) year = models.PositiveSmallIntegerField(verbose_name=_("Year"), blank=True, null=True) source = models.CharField(max_length=50, choices=Sources.choices, verbose_name=_("Source")) channel = models.CharField(max_length=50, choices=Channel.choices, verbose_name=_("Channel")) - address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) city = models.CharField(max_length=50, verbose_name=_("City")) - assigned = models.ForeignKey(Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned")) + staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned")) priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.MEDIUM, verbose_name=_("Priority")) status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True, default=Status.NEW) @@ -826,38 +885,6 @@ class LeadStatusHistory(models.Model): return f"{self.lead}: {self.old_status} → {self.new_status}" -class Customer(models.Model): - lead = models.OneToOneField(Lead, on_delete=models.SET_NULL, null=True, blank=True, - related_name="converted", verbose_name=_("Lead")) - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE,related_name="customers") - staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="customer_staff", - verbose_name=_("Staff")) - 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")) - last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) - gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender")) - dob = models.DateField(verbose_name=_("Date of Birth")) - email = models.EmailField(unique=True, verbose_name=_("Email")) - national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID")) - phone_number = PhoneNumberField(region="SA", unique=True, verbose_name=_("Phone Number")) - city = models.CharField(max_length=255, blank=True, verbose_name=_("City")) - address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) - created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) - updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) - - class Meta: - verbose_name = _("Customer") - 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}" - - @property - def get_full_name(self): - return f"{self.first_name} {self.middle_name} {self.last_name}" - def validate_probability(value): if value < 0 or value > 100: raise ValidationError(_("Probability must be between 0 and 100.")) @@ -963,41 +990,6 @@ class Vendor(models.Model, LocalizedNameMixin): return self.name -class Organization(models.Model, LocalizedNameMixin): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations') - name = models.CharField(max_length=255, verbose_name=_("Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) - crn = models.CharField(max_length=15, verbose_name=_("Commercial Registration Number")) - vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) - phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) - address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) - logo = models.ImageField(upload_to="logos", blank=True, null=True, verbose_name=_("Logo")) - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) - - class Meta: - verbose_name = _("Organization") - verbose_name_plural = _("Organizations") - def __str__(self): - return self.name - - -class Representative(models.Model, LocalizedNameMixin): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='representatives') - name = models.CharField(max_length=255, verbose_name=_("Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) - id_number = models.CharField(max_length=10, verbose_name=_("ID Number")) - phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number")) - email = models.EmailField(max_length=255, verbose_name=_("Email Address")) - address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) - organization = models.ManyToManyField(Organization, related_name='representatives') - - class Meta: - verbose_name = _("Representative") - verbose_name_plural = _("Representatives") - - def __str__(self): - return self.name - class SaleQuotation(models.Model): quotation_number = models.CharField(max_length=10, unique=True) diff --git a/inventory/signals.py b/inventory/signals.py index b00f31f5..2cc18ff8 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -805,7 +805,7 @@ def track_lead_status_change(sender, instance, **kwargs): lead=instance, old_status=old_lead.status, new_status=instance.status, - changed_by=instance.assigned # Assuming the assigned staff made the change + changed_by=instance.staff # Assuming the assigned staff made the change ) except models.Lead.DoesNotExist: pass # Ignore if the lead doesn't exist (e.g., during initial creation) @@ -813,10 +813,10 @@ def track_lead_status_change(sender, instance, **kwargs): @receiver(post_save, sender=models.Lead) def notify_assigned_staff(sender, instance, created, **kwargs): - if instance.assigned: # Check if the lead is assigned + if instance.staff: # Check if the lead is assigned models.Notification.objects.create( - user=instance.assigned.user, - message=f"You have been assigned a new lead: {instance.first_name} {instance.last_name}." + user=instance.staff.user, + message=f"You have been assigned a new lead: {instance.customer.get_full_name}." ) diff --git a/inventory/views.py b/inventory/views.py index 23f17d27..c1d08243 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -884,6 +884,7 @@ class CustomerListView(LoginRequiredMixin, ListView): context_object_name = "customers" paginate_by = 10 template_name = "customers/customer_list.html" + ordering = ["-created"] def get_queryset(self): query = self.request.GET.get("q") @@ -1001,6 +1002,11 @@ class VendorListView(LoginRequiredMixin, ListView): context_object_name = "vendors" paginate_by = 10 template_name = "vendors/vendors_list.html" + ordering = ["-created"] + + def get_queryset(self): + dealer = get_user_type(self.request) + return models.Vendor.objects.filter(dealer=dealer) class VendorDetailView(LoginRequiredMixin, DetailView): @@ -1430,8 +1436,7 @@ class UserListView(LoginRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) - staff = models.Staff.objects.filter(dealer=dealer).all() - return staff + return models.Staff.objects.filter(dealer=dealer).all() class UserDetailView(LoginRequiredMixin, DetailView): @@ -1514,8 +1519,7 @@ class OrganizationListView(LoginRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) - data = models.Organization.objects.filter(dealer=dealer).all() - return data + return models.Organization.objects.filter(dealer=dealer).all() class OrganizationDetailView(DetailView): @@ -1559,11 +1563,11 @@ class RepresentativeListView(LoginRequiredMixin, ListView): model = models.Representative template_name = "representatives/representative_list.html" context_object_name = "representatives" + paginate_by = 10 def get_queryset(self): dealer = get_user_type(self.request) - data = models.Representative.objects.filter(dealer=dealer).all() - return data + return models.Representative.objects.filter(dealer=dealer).all() class RepresentativeDetailView(DetailView): @@ -1785,6 +1789,7 @@ class BankAccountListView(LoginRequiredMixin, ListView): model = BankAccountModel template_name = "ledger/bank_accounts/bank_account_list.html" context_object_name = "bank_accounts" + paginate_by = 10 def get_queryset(self): dealer = get_user_type(self.request) @@ -1860,11 +1865,7 @@ class AccountListView(LoginRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) entity = dealer.entity - qs = entity.get_all_accounts() - paginator = Paginator(qs, 20) - page_number = self.request.GET.get("page", 1) # Default to page 1 - page_obj = paginator.get_page(page_number) - return page_obj + return entity.get_all_accounts() class AccountCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): @@ -2222,6 +2223,7 @@ class InvoiceListView(LoginRequiredMixin, ListView): model = InvoiceModel template_name = "sales/invoices/invoice_list.html" context_object_name = "invoices" + paginate_by = 20 def get_queryset(self): dealer = get_user_type(self.request) @@ -2583,7 +2585,7 @@ class UserActivityLogListView(ListView): model = models.UserActivityLog template_name = "dealers/activity_log.html" context_object_name = "logs" - paginate_by = 10 + paginate_by = 20 def get_queryset(self): queryset = super().get_queryset() @@ -2601,8 +2603,7 @@ class LeadListView(ListView): def get_queryset(self): dealer = get_user_type(self.request) - leads = models.Lead.objects.filter(dealer=dealer).all() - return leads + return models.Lead.objects.filter(dealer=dealer).all() class LeadDetailView(DetailView): @@ -2706,10 +2707,10 @@ class OpportunityCreateView(CreateView): def form_valid(self, form): dealer = get_user_type(self.request) form.instance.dealer = dealer - staff = self.request.user.staff + # staff = dealer.staff print(dealer) - print(staff) - form.instance.staff = staff + # print(staff) + # form.instance.staff = staff return super().form_valid(form) def get_success_url(self): @@ -2735,11 +2736,11 @@ class OpportunityListView(ListView): model = models.Opportunity template_name = "crm/opportunities/opportunity_list.html" context_object_name = "opportunities" + paginate_by = 10 def get_queryset(self): dealer = get_user_type(self.request) - data = models.Opportunity.objects.filter(dealer=dealer).all() - return data + return models.Opportunity.objects.filter(dealer=dealer).all() @login_required @@ -2771,12 +2772,11 @@ class NotificationListView(LoginRequiredMixin, ListView): model = models.Notification template_name = "crm/notifications_history.html" context_object_name = "notifications" - paginate_by = 10 + paginate_by = 20 + ordering = "-created" def get_queryset(self): - return models.Notification.objects.filter(user=self.request.user).order_by( - "-created" - ) + return models.Notification.objects.filter(user=self.request.user) @login_required @@ -2840,11 +2840,11 @@ class ItemServiceListView(ListView): model = models.AdditionalServices template_name = "items/service/service_list.html" context_object_name = "services" + paginate_by = 20 def get_queryset(self): dealer = get_user_type(self.request) - items = models.AdditionalServices.objects.filter(dealer=dealer).all() - return items + return models.AdditionalServices.objects.filter(dealer=dealer).all() class ItemExpenseCreateView(CreateView): @@ -2889,22 +2889,22 @@ class ItemExpenseListView(ListView): model = ItemModel template_name = "items/expenses/expenses_list.html" context_object_name = "expenses" + paginate_by = 20 def get_queryset(self): dealer = get_user_type(self.request) - items = dealer.entity.get_items_expenses() - return items + return dealer.entity.get_items_expenses() class BillListView(ListView): model = ItemModel template_name = "ledger/bills/bill_list.html" context_object_name = "bills" + paginate_by = 20 def get_queryset(self): dealer = get_user_type(self.request) - items = dealer.entity.get_bills() - return items + return dealer.entity.get_bills() class BillCreateView(LoginRequiredMixin,SuccessMessageMixin,CreateView): diff --git a/static/images/.DS_Store b/static/images/.DS_Store index 2b8dc0e8..f3dca2fb 100644 Binary files a/static/images/.DS_Store and b/static/images/.DS_Store differ diff --git a/static/images/car_make/maserati_Fw9s7lU.png b/static/images/car_make/maserati_Fw9s7lU.png new file mode 100644 index 00000000..7cb2e6e3 Binary files /dev/null and b/static/images/car_make/maserati_Fw9s7lU.png differ diff --git a/static/images/spot-illustrations/.DS_Store b/static/images/spot-illustrations/.DS_Store index da69a15d..e49b58b0 100644 Binary files a/static/images/spot-illustrations/.DS_Store and b/static/images/spot-illustrations/.DS_Store differ diff --git a/templates/crm/.DS_Store b/templates/crm/.DS_Store index 61e33263..1905f30c 100644 Binary files a/templates/crm/.DS_Store and b/templates/crm/.DS_Store differ diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html index 62dc71a6..e3367738 100644 --- a/templates/crm/leads/lead_list.html +++ b/templates/crm/leads/lead_list.html @@ -119,11 +119,11 @@