From 8e6d62273dd74e11c24fb435e14991cf5236aac3 Mon Sep 17 00:00:00 2001 From: ismail <=> Date: Wed, 21 May 2025 19:26:41 +0300 Subject: [PATCH] add admin management+more fixes to the overall logic + lint and formating + more --- api/services.py | 2 - api/tests.py | 1 - api/views.py | 6 +- haikalbot/admin.py | 1 - haikalbot/tests.py | 1 - inventory/admin.py | 1 - inventory/apps.py | 2 +- inventory/forms.py | 7 +- inventory/haikalna.py | 5 +- inventory/management/commands/db2json.py | 2 - inventory/management/commands/generate_vin.py | 12 +- inventory/management/commands/setplan.py | 2 +- inventory/management/commands/tenhal_plan.py | 7 +- inventory/management/commands/test.py | 4 - inventory/middleware.py | 10 +- .../migrations/0006_organization_slug.py | 18 + .../0007_staff_active_staff_slug.py | 23 + inventory/migrations/0008_lead_salary.py | 18 + inventory/mixins.py | 2 - inventory/models.py | 84 +- inventory/services.py | 6 +- inventory/signals.py | 19 +- inventory/tables.py | 2 +- inventory/tasks.py | 3 +- inventory/templatetags/custom_filters.py | 1 - inventory/templatetags/tenhal_tag.py | 5 +- inventory/tests.py | 2 - inventory/urls.py | 36 +- inventory/utilities/financials.py | 1 - inventory/views.py | 894 +++++++++++------- merge_db.py | 1 - scripts/new_wmis.py | 1 - scripts/r.py | 7 +- scripts/run.py | 28 - scripts/run1.py | 19 +- scripts/run2.py | 17 +- .../confirm_activate_account.html | 17 + templates/admin_management/management.html | 16 + .../permenant_delete_account.html | 18 + .../admin_management/user_management.html | 243 +++++ templates/crm/leads/lead_detail.html | 286 +++--- templates/crm/leads/lead_form.html | 28 +- templates/crm/leads/lead_list.html | 4 +- .../partials/opportunity_grid.html | 30 +- templates/customers/customer_form.html | 2 +- templates/dealers/dealer_detail.html | 8 +- templates/header.html | 5 + templates/inventory/add_colors.html | 4 +- templates/inventory/car_finance_form.html | 14 +- templates/inventory/color_palette.html | 2 +- templates/items/expenses/expense_create.html | 2 +- templates/items/service/service_create.html | 2 +- .../bank_accounts/bank_account_form.html | 58 +- .../bank_accounts/bank_account_list.html | 6 +- templates/ledger/bills/bill_form.html | 2 +- .../ledger/coa_accounts/account_form.html | 4 +- templates/ledger/ledger/ledger_form.html | 6 +- .../organizations/organization_detail.html | 2 +- .../organizations/organization_list.html | 6 +- templates/payment_failed.html | 2 +- templates/pricing_page.html | 30 +- templates/sales/estimates/estimate_form.html | 10 +- templates/sales/invoices/invoice_list.html | 2 +- templates/users/user_detail.html | 6 +- templates/users/user_group_form.html | 4 - templates/users/user_list.html | 16 +- templates/vendors/vendor_form.html | 8 +- templates/vendors/vendors_list.html | 156 +-- 68 files changed, 1401 insertions(+), 848 deletions(-) create mode 100644 inventory/migrations/0006_organization_slug.py create mode 100644 inventory/migrations/0007_staff_active_staff_slug.py create mode 100644 inventory/migrations/0008_lead_salary.py create mode 100644 templates/admin_management/confirm_activate_account.html create mode 100644 templates/admin_management/management.html create mode 100644 templates/admin_management/permenant_delete_account.html create mode 100644 templates/admin_management/user_management.html diff --git a/api/services.py b/api/services.py index c36bacc7..7284a869 100644 --- a/api/services.py +++ b/api/services.py @@ -1,5 +1,3 @@ -import hashlib -import json import requests diff --git a/api/tests.py b/api/tests.py index c2629a3a..42ddf071 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase # Create your tests here. \ No newline at end of file diff --git a/api/views.py b/api/views.py index 11a40d81..1031e3a9 100644 --- a/api/views.py +++ b/api/views.py @@ -1,17 +1,15 @@ from django.core.paginator import Paginator from django.http import JsonResponse -from rest_framework import permissions, status, viewsets, generics +from rest_framework import permissions, status, viewsets from inventory.utils import get_user_type from . import models, serializers from .services import get_car_data from inventory import models as inventory_models from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework import status from django.utils.translation import gettext as _ -from inventory import models as inventory_models -from inventory.services import get_make, get_model, decodevin +from inventory.services import decodevin import logging logger = logging.getLogger(__name__) diff --git a/haikalbot/admin.py b/haikalbot/admin.py index 8c38f3f3..b97a94f6 100644 --- a/haikalbot/admin.py +++ b/haikalbot/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin # Register your models here. diff --git a/haikalbot/tests.py b/haikalbot/tests.py index 7ce503c2..49290204 100644 --- a/haikalbot/tests.py +++ b/haikalbot/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase # Create your tests here. diff --git a/inventory/admin.py b/inventory/admin.py index 1435e3d0..6983bb84 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -7,7 +7,6 @@ from django_ledger import models as ledger_models from import_export.admin import ExportMixin from import_export.resources import ModelResource -from .models import Car # # Define resource class # class CarSerieResource(ModelResource): diff --git a/inventory/apps.py b/inventory/apps.py index 7a84f50f..10d8c5bd 100644 --- a/inventory/apps.py +++ b/inventory/apps.py @@ -5,7 +5,7 @@ class InventoryConfig(AppConfig): name = 'inventory' def ready(self): - import inventory.signals + pass #from decimal import Decimal #from inventory.models import VatRate #VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True) diff --git a/inventory/forms.py b/inventory/forms.py index 05d7df1a..faa9fd72 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -1,11 +1,8 @@ -import re from django.core.cache import cache from datetime import datetime from luhnchecker.luhn import Luhn -from django.core.validators import RegexValidator from django.contrib.auth.models import Permission from appointment.models import Service -from phonenumber_field.formfields import PhoneNumberField from django.core.validators import MinLengthValidator from django import forms from plans.models import PlanPricing @@ -17,9 +14,6 @@ from .mixins import AddClassMixin from django_ledger.forms.invoice import ( InvoiceModelCreateForm as InvoiceModelCreateFormBase, ) -from django_ledger.forms.estimate import ( - EstimateModelCreateForm as EstimateModelCreateFormBase, -) from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase from django_ledger.forms.journal_entry import JournalEntryModelCreateForm as JournalEntryModelCreateFormBase @@ -1062,6 +1056,7 @@ class LeadForm(forms.ModelForm): "crn", "vrn", "year", + "salary", "source", "channel", "staff", diff --git a/inventory/haikalna.py b/inventory/haikalna.py index e2475e5f..7d4e9297 100644 --- a/inventory/haikalna.py +++ b/inventory/haikalna.py @@ -1,7 +1,4 @@ import re -from itertools import cycle -from datetime import datetime -from typing import List def vin_year(vin_char: str) -> int: @@ -1830,7 +1827,7 @@ def decode_vin_haikalna(vin): pattern = r"^[A-HJ-NPR-Z0-9]{17}$" if not re.match(pattern, vin): - raise Exception(f"VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ") + raise Exception("VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ") vin = vin.upper() diff --git a/inventory/management/commands/db2json.py b/inventory/management/commands/db2json.py index 6d633316..d28ea03e 100644 --- a/inventory/management/commands/db2json.py +++ b/inventory/management/commands/db2json.py @@ -1,11 +1,9 @@ -import os import json import pymysql import pandas as pd from django.core.management.base import BaseCommand from sqlalchemy import create_engine from tqdm import tqdm # Progress bar support -from django.conf import settings # Database connection details db_config = { diff --git a/inventory/management/commands/generate_vin.py b/inventory/management/commands/generate_vin.py index b7d6ba44..bb1a4186 100644 --- a/inventory/management/commands/generate_vin.py +++ b/inventory/management/commands/generate_vin.py @@ -11,19 +11,19 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): vin,description = self.generate_vin() result = decodevin(vin) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}')) self.stdout.write(self.style.SUCCESS(f'Description: {description}')) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(self.style.SUCCESS(f'Decoded VIN: {result}')) make,model,year_model = result.values() self.stdout.write(self.style.SUCCESS(f'VIN: {vin} - Make {make} - Model {model} - Model Year {year_model}')) m = get_model(model) self.stdout.write(self.style.SUCCESS(f'Make: {m.id_car_make} - Model: {m}')) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) - self.stdout.write(self.style.SUCCESS(f'####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) + self.stdout.write(self.style.SUCCESS('####################################################################################################')) diff --git a/inventory/management/commands/setplan.py b/inventory/management/commands/setplan.py index 9b6cb860..e168d5ef 100644 --- a/inventory/management/commands/setplan.py +++ b/inventory/management/commands/setplan.py @@ -1,6 +1,6 @@ # management/commands/create_plans.py from django.core.management.base import BaseCommand -from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing +from plans.models import Plan, Quota, Pricing, PlanPricing from decimal import Decimal from django.db.models import Q diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index 91b912e3..acd3d39b 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -1,12 +1,7 @@ # management/commands/create_plans.py from decimal import Decimal -from datetime import timedelta -from django.db.models import Q -from django.utils import timezone -from plans.quota import get_user_quota -from django.contrib.auth.models import User from django.core.management.base import BaseCommand -from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing,UserPlan,Order,BillingInfo,AbstractOrder +from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing class Command(BaseCommand): help = 'Create basic subscription plans structure' diff --git a/inventory/management/commands/test.py b/inventory/management/commands/test.py index 889b7d12..2f2b7326 100644 --- a/inventory/management/commands/test.py +++ b/inventory/management/commands/test.py @@ -1,9 +1,5 @@ from django.core.management.base import BaseCommand -from django.core.mail import send_mail -from allauth.account.models import EmailConfirmation -from inventory.tasks import send_email from django.contrib.auth import get_user_model -import re from inventory.tasks import create_coa_accounts from inventory.models import Dealer diff --git a/inventory/middleware.py b/inventory/middleware.py index 4e65046a..8a65685b 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -1,12 +1,6 @@ import logging -from django.conf import settings from inventory import models from django.utils import timezone -from django.shortcuts import redirect -from django.urls import reverse -from django.utils.deprecation import MiddlewareMixin -from fpdf import FPDF -import os from inventory.utils import get_user_type @@ -67,7 +61,7 @@ class InjectParamsMiddleware: try: # request.entity = request.user.dealer.entity request.dealer = get_user_type(request) - except Exception as e: + except Exception: pass response = self.get_response(request) return response @@ -98,7 +92,7 @@ class InjectDealerMiddleware: request.is_dealer = True if hasattr(request.user, "staffmember"): request.is_staff = True - except Exception as e: + except Exception: pass response = self.get_response(request) return response diff --git a/inventory/migrations/0006_organization_slug.py b/inventory/migrations/0006_organization_slug.py new file mode 100644 index 00000000..91b349cf --- /dev/null +++ b/inventory/migrations/0006_organization_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-05-21 10:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0005_notes_dealer'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='slug', + field=models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True), + ), + ] diff --git a/inventory/migrations/0007_staff_active_staff_slug.py b/inventory/migrations/0007_staff_active_staff_slug.py new file mode 100644 index 00000000..704dddbf --- /dev/null +++ b/inventory/migrations/0007_staff_active_staff_slug.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.7 on 2025-05-21 13:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0006_organization_slug'), + ] + + operations = [ + migrations.AddField( + model_name='staff', + name='active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + migrations.AddField( + model_name='staff', + name='slug', + field=models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True), + ), + ] diff --git a/inventory/migrations/0008_lead_salary.py b/inventory/migrations/0008_lead_salary.py new file mode 100644 index 00000000..ea22048f --- /dev/null +++ b/inventory/migrations/0008_lead_salary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-05-21 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0007_staff_active_staff_slug'), + ] + + operations = [ + migrations.AddField( + model_name='lead', + name='salary', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary'), + ), + ] diff --git a/inventory/mixins.py b/inventory/mixins.py index ffc728cf..9a6f9c77 100644 --- a/inventory/mixins.py +++ b/inventory/mixins.py @@ -1,6 +1,4 @@ -from django import forms from django.utils.translation import get_language -from django.urls import reverse, reverse_lazy class AddClassMixin: """ diff --git a/inventory/models.py b/inventory/models.py index c4df99f5..a4ecff98 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import Permission from decimal import Decimal from django.utils.text import slugify from django.utils import timezone -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MinValueValidator import hashlib from django.db import models from datetime import timedelta @@ -23,12 +23,12 @@ from django.contrib.auth.models import Group from inventory.utils import get_user_type, to_dict from .mixins import LocalizedNameMixin -from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel,AccountModel,EntityManagementModel +from django_ledger.models import EstimateModel,InvoiceModel,AccountModel,EntityManagementModel from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from appointment.models import StaffMember from plans.quota import get_user_quota -from plans.models import UserPlan,Quota,PlanQuota +from plans.models import UserPlan # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords @@ -548,7 +548,7 @@ class Car(Base): def ready(self): try: return all([self.colors ,self.finances,]) - except Exception as e: + except Exception: return False def get_transfer(self): return self.transfer_logs.filter(active=True).first() @@ -1000,11 +1000,37 @@ class Staff(models.Model, LocalizedNameMixin): arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) staff_type = models.CharField(choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")) + active = models.BooleanField(default=True, verbose_name=_("Active")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + slug = models.SlugField(max_length=255, unique=True, editable=False, null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.slug: + base_slug = slugify(f"{self.name}") + self.slug = base_slug + counter = 1 + + while self.__class__.objects.filter(slug=self.slug).exclude(pk=self.pk).exists(): + self.slug = f"{base_slug}-{counter}" + counter += 1 + super().save(*args, **kwargs) objects = StaffUserManager() - + def deactivate_account(self): + self.active = False + self.user.is_active = False + self.user.save() + self.save() + def activate_account(self): + self.active = True + self.user.is_active = True + self.user.save() + self.save() + def permenant_delete(self): + # self.user.delete() + self.staff_member.delete() + self.delete() @property def email(self): return self.staff_member.user.email @@ -1256,7 +1282,17 @@ class Customer(models.Model): self.customer_model.save() self.user.save() self.save() - + def activate_account(self): + self.active = True + self.customer_model.active = True + self.user.is_active = True + self.customer_model.save() + self.user.save() + self.save() + def permenant_delete(self): + self.customer_model.delete() + self.user.delete() + self.delete() class Organization(models.Model, LocalizedNameMixin): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="organizations" @@ -1282,6 +1318,18 @@ class Organization(models.Model, LocalizedNameMixin): active = models.BooleanField(default=True, verbose_name=_("Active")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + slug = models.SlugField(max_length=255, unique=True, editable=False, null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.slug: + base_slug = slugify(f"{self.name}") + self.slug = base_slug + counter = 1 + + while self.__class__.objects.filter(slug=self.slug).exclude(pk=self.pk).exists(): + self.slug = f"{base_slug}-{counter}" + counter += 1 + super().save(*args, **kwargs) class Meta: verbose_name = _("Organization") @@ -1348,6 +1396,17 @@ class Organization(models.Model, LocalizedNameMixin): self.user.save() self.customer_model.save() self.save() + def activate_account(self): + self.active = True + self.customer_model.active = True + self.user.is_active = True + self.customer_model.save() + self.user.save() + self.save() + def permenant_delete(self): + self.user.delete() + self.customer_model.delete() + self.delete() class Representative(models.Model, LocalizedNameMixin): dealer = models.ForeignKey( @@ -1447,7 +1506,6 @@ class Lead(models.Model): blank=True, null=True ) - next_action_date = models.DateTimeField( verbose_name=_("Next Action Date"), blank=True, @@ -1455,6 +1513,7 @@ class Lead(models.Model): ) is_converted = models.BooleanField(default=False) converted_at = models.DateTimeField(null=True, blank=True) + salary = models.PositiveIntegerField(verbose_name=_("Salary"), blank=True, null=True) created = models.DateTimeField( auto_now_add=True, verbose_name=_("Created"), db_index=True ) @@ -1548,7 +1607,6 @@ class Lead(models.Model): return Activity.objects.filter(dealer=self.dealer,content_type__model="lead", object_id=self.pk).order_by('-updated').first() def save(self, *args, **kwargs): - self.status = self.get_status() if not self.slug: base_slug = slugify(f"{self.last_name} {self.first_name}") self.slug = base_slug @@ -1882,7 +1940,13 @@ class Vendor(models.Model, LocalizedNameMixin): balance_type="credit", active=True ) - + def activate_account(self): + self.active = True + self.vendor_model.active = True + self.save() + def permenant_delete(self): + self.vendor_model.delete() + self.delete() class Payment(models.Model): METHOD_CHOICES = [ ("cash", _("cash")), @@ -2034,7 +2098,7 @@ class CustomGroup(models.Model): try: for perm in Permission.objects.filter(content_type__app_label="inventory"): self.add_permission(perm) - except Exception as e: + except Exception: pass def set_default_permissions(self): diff --git a/inventory/services.py b/inventory/services.py index ea9691b8..9124f968 100644 --- a/inventory/services.py +++ b/inventory/services.py @@ -5,14 +5,10 @@ Services module import requests import json -from django_ledger.models import EntityModel -from inventory.utils import get_jwt_token, get_user_type from pyvin import VIN from django.conf import settings -from openai import OpenAI -from .models import Car,CarMake,CarModel -from inventory.haikalna import decode_vin_haikalna +from .models import CarMake def get_make(item): diff --git a/inventory/signals.py b/inventory/signals.py index 4a94604b..2334a652 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -1,33 +1,20 @@ -from inventory.tasks import create_coa_accounts,create_accounts_for_make,create_settings +from inventory.tasks import create_coa_accounts from django.contrib.auth.models import Group -from decimal import Decimal -from django.db.models.signals import post_save, post_delete, pre_delete, pre_save -from inventory.models import VatRate -from plans.quota import get_user_quota -from .utils import to_dict +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model from django_ledger.io import roles from django_ledger.models import ( EntityModel, - AccountModel, ItemModel, - ItemModelAbstract, - UnitOfMeasureModel, - VendorModel, - EstimateModel, - CustomerModel, JournalEntryModel, TransactionModel, - LedgerModel, - BillModel, - ItemTransactionModel + LedgerModel ) from . import models from django.utils.timezone import now from django.db import transaction -from django.core.exceptions import ValidationError User = get_user_model() diff --git a/inventory/tables.py b/inventory/tables.py index 33df7b5c..84881c70 100644 --- a/inventory/tables.py +++ b/inventory/tables.py @@ -2,7 +2,7 @@ from django.conf import settings from django.utils.timesince import timesince import django_tables2 as tables from django.utils.translation import gettext_lazy as _ -from .models import Car, CarFinance, ExteriorColors, InteriorColors, CarColors +from .models import Car from .utils import get_local_name from django.utils.html import format_html diff --git a/inventory/tasks.py b/inventory/tasks.py index 675d59b3..c608a551 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -1,11 +1,10 @@ -from time import sleep from datetime import datetime from django.db import transaction from django_ledger.io import roles from django.core.mail import send_mail from background_task import background from django.utils.translation import gettext_lazy as _ -from inventory.models import DealerSettings,CarMake,Dealer +from inventory.models import DealerSettings,Dealer diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 6c0f97e7..85906f9f 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -5,7 +5,6 @@ from django.urls import reverse from django.utils.formats import number_format from django_ledger.io.io_core import get_localdate,validate_activity from django.conf import settings -from django.utils.translation import get_language register = template.Library() diff --git a/inventory/templatetags/tenhal_tag.py b/inventory/templatetags/tenhal_tag.py index 14e45fa2..ed862964 100644 --- a/inventory/templatetags/tenhal_tag.py +++ b/inventory/templatetags/tenhal_tag.py @@ -7,19 +7,16 @@ # """ from calendar import month_abbr -from random import randint from django import template -from django.db.models import Sum from django.urls import reverse -from django.utils.formats import number_format from decimal import Decimal # from django_ledger import __version__ # from django_ledger.forms.app_filters import EntityFilterForm, ActivityFilterForm # from django_ledger.forms.feedback import BugReportForm, RequestNewFeatureForm # from django_ledger.io import CREDIT, DEBIT, ROLES_ORDER_ALL -from django_ledger.io.io_core import validate_activity, get_localdate +from django_ledger.io.io_core import get_localdate # from django_ledger.models import TransactionModel, BillModel, InvoiceModel, EntityUnitModel # from django_ledger.settings import ( # DJANGO_LEDGER_FINANCIAL_ANALYSIS, DJANGO_LEDGER_CURRENCY_SYMBOL, diff --git a/inventory/tests.py b/inventory/tests.py index 38555717..aa2da193 100644 --- a/inventory/tests.py +++ b/inventory/tests.py @@ -1,8 +1,6 @@ import json from . import models as m -from datetime import datetime from django.urls import reverse -from django_ledger import models as lm from django.test import Client, TestCase from django.contrib.auth import get_user_model from django_ledger.io.io_core import get_localdate diff --git a/inventory/urls.py b/inventory/urls.py index 00aaf1f3..9824c840 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -3,7 +3,6 @@ from django.urls import path from django_tables2.export.export import TableExport from . import views -from allauth.account import views as allauth_views urlpatterns = [ @@ -395,12 +394,12 @@ path( # ), # Users URLs - path("user/create/", views.UserCreateView.as_view(), name="user_create"), - path("user//update/", views.UserUpdateView.as_view(), name="user_update"), - path("user//", views.UserDetailView.as_view(), name="user_detail"), path("user/", views.UserListView.as_view(), name="user_list"), - path("user//confirm/", views.UserDeleteview, name="user_delete"), - path("user//groups/", views.UserGroupView, name="user_groups"), + path("user/create/", views.UserCreateView.as_view(), name="user_create"), + path("user//", views.UserDetailView.as_view(), name="user_detail"), + path("user//groups/", views.UserGroupView, name="user_groups"), + path("user//update/", views.UserUpdateView.as_view(), name="user_update"), + path("user//confirm/", views.UserDeleteview, name="user_delete"), # Group URLs path("group/create/", views.GroupCreateView.as_view(), name="group_create"), path("group//update/", views.GroupUpdateView.as_view(), name="group_update"), @@ -409,26 +408,26 @@ path( path("group//confirm/", views.GroupDeleteview, name="group_delete"), path("group//permission/", views.GroupPermissionView, name="group_permission"), # Organization URLs - path( - "organizations/", views.OrganizationListView.as_view(), name="organization_list" - ), - path( - "organizations//", - views.OrganizationDetailView.as_view(), - name="organization_detail", - ), path( "organizations/create/", views.OrganizationCreateView.as_view(), name="organization_create", ), path( - "organizations//update/", + "organizations/", views.OrganizationListView.as_view(), name="organization_list" + ), + path( + "organizations//", + views.OrganizationDetailView.as_view(), + name="organization_detail", + ), + path( + "organizations//update/", views.OrganizationUpdateView.as_view(), name="organization_update", ), path( - "organizations//delete/", + "organizations//delete/", views.OrganizationDeleteView, name="organization_delete", ), @@ -802,6 +801,11 @@ path( path('entity//data/pnl/', views.PnLAPIView.as_view(), name='entity-json-pnl'), + # Admin Management... + path('management/', views.management_view, name='management'), + path('management/user_management/', views.user_management, name='user_management'), + path('management///activate_account/', views.activate_account, name='activate_account'), + path('management///permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'), ] diff --git a/inventory/utilities/financials.py b/inventory/utilities/financials.py index 8cfe0e2a..fab7a159 100644 --- a/inventory/utilities/financials.py +++ b/inventory/utilities/financials.py @@ -1,7 +1,6 @@ from decimal import Decimal from django.conf import settings -from inventory import models from django_ledger.models.items import ItemModel def calculate_vat(value): """Helper to calculate VAT dynamically for a given value.""" diff --git a/inventory/views.py b/inventory/views.py index d3868453..5b20138c 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5,24 +5,25 @@ import logging from datetime import datetime from time import sleep import numpy as np + # from rich import print from random import randint from decimal import Decimal from django.apps import apps -from datetime import timedelta from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse + ##################################################################### from inventory.models import Status as LeadStatus from background_task.models import Task from django.db.models.deletion import RestrictedError from django.http.response import StreamingHttpResponse + # Django from django.db.models import Q from django.conf import settings -from django.db import IntegrityError, transaction from django.db.models import Func from django.contrib import messages from django.http import Http404, JsonResponse, HttpResponseForbidden @@ -33,14 +34,13 @@ from django.db.models import Sum, F, Count from django.core.paginator import Paginator from django.contrib.auth.models import User from django.contrib.auth.models import Group -from django.db.models import Count, F, Value +from django.db.models import Value from django.urls import reverse, reverse_lazy from django.utils import timezone, translation from django.db.models.functions import Coalesce from django.contrib.auth.models import Permission from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt -from django.core.files.storage import default_storage from django.utils.translation import gettext_lazy as _ from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin @@ -49,11 +49,10 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.decorators import permission_required from django.shortcuts import render, get_object_or_404, redirect -from plans.models import Order,PlanPricing,AbstractOrder,UserPlan,BillingInfo +from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo from django.views.generic import ( View, ListView, - DetailView, CreateView, UpdateView, DeleteView, @@ -64,25 +63,11 @@ from django.views.generic import ( # Django Ledger from django_ledger.io import roles from django_ledger.utils import accruable_net_summary -from django_ledger.views.invoice import ( - InvoiceModelDetailView as InvoiceModelDetailViewBase, -) from django_ledger.views import ( - LedgerModelListView as LedgerModelListViewBase, JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase, LedgerModelModelActionView as LedgerModelModelActionViewBase, LedgerModelDeleteView as LedgerModelDeleteViewBase, - LedgerModelCreateView as LedgerModelCreateViewBase -) -from django_ledger.views.invoice import ( - InvoiceModelDetailView as InvoiceModelDetailViewBase, -) -from django_ledger.views import ( - LedgerModelListView as LedgerModelListViewBase, - JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase, - LedgerModelModelActionView as LedgerModelModelActionViewBase, - LedgerModelDeleteView as LedgerModelDeleteViewBase, - LedgerModelCreateView as LedgerModelCreateViewBase + LedgerModelCreateView as LedgerModelCreateViewBase, ) from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm from django_ledger.views.entity import ( @@ -90,11 +75,6 @@ from django_ledger.views.entity import ( EntityModelDetailHandlerView, ) from django_ledger.forms.ledger import LedgerModelCreateForm -from django_ledger.views.entity import ( - EntityModelDetailBaseView, - EntityModelDetailHandlerView, -) -from django_ledger.forms.ledger import LedgerModelCreateForm from django_ledger.forms.item import ( ExpenseItemCreateForm, ExpenseItemUpdateForm, @@ -125,7 +105,6 @@ from django_ledger.models import ( ItemModel, BillModel, LedgerModel, - VendorModel, ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -136,7 +115,6 @@ from django_ledger.views.financial_statement import ( ) from django_ledger.io.io_core import get_localdate -from django_ledger.models import EntityModel from django_ledger.views.mixins import ( QuarterlyReportMixIn, MonthlyReportMixIn, @@ -144,17 +122,14 @@ from django_ledger.views.mixins import ( DjangoLedgerSecurityMixIn, EntityUnitMixIn, ) + # Other from plans.models import Plan -from inventory.filters import AccountModelFilter from . import models, forms, tables -from plans.quota import get_user_quota from django_tables2 import SingleTableView from django_tables2.export.views import ExportMixin from appointment.models import Appointment, AppointmentRequest, Service, StaffMember -from django.db.models.functions import Lower -from .models import SaleOrder from .services import ( decodevin, get_make, @@ -164,7 +139,6 @@ from .utils import ( CarFinanceCalculator, create_user_dealer, get_car_finance_data, - get_financial_values, get_item_transactions, handle_payment, reserve_car, @@ -181,7 +155,6 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) - class Hash(Func): """ Represents a function used to compute a hash value. @@ -195,6 +168,7 @@ class Hash(Func): :ivar function: Specifies the hash computation function. :type function: str """ + function = "get_hash" @@ -295,10 +269,10 @@ def dealer_signup(request, *args, **kwargs): if password != password_confirm: return JsonResponse({"error": _("Passwords do not match")}, status=400) try: - create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address) - return JsonResponse( - {"message": _("User created successfully")}, status=200 - ) + create_user_dealer( + email, password, name, arabic_name, phone, crn, vrn, address + ) + return JsonResponse({"message": _("User created successfully")}, status=200) except Exception as e: return JsonResponse({"error": str(e)}, status=400) return render( @@ -308,7 +282,6 @@ def dealer_signup(request, *args, **kwargs): ) - class HomeView(LoginRequiredMixin, TemplateView): """ HomeView class responsible for rendering the home page. @@ -324,6 +297,7 @@ class HomeView(LoginRequiredMixin, TemplateView): output. :type template_name: str """ + template_name = "index.html" def dispatch(self, request, *args, **kwargs): @@ -346,6 +320,7 @@ class TestView(TemplateView): rendering the cars list view. :type template_name: str """ + template_name = "inventory/cars_list_api.html" @@ -362,13 +337,16 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView): :ivar template_name: Path to the template used for rendering the manager's dashboard. :type template_name: str """ + template_name = "dashboards/manager.html" def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return redirect("welcome") - if not getattr(request.user, 'dealer', False): - return HttpResponseForbidden("You are not authorized to view this dashboard.") + if not getattr(request.user, "dealer", False): + return HttpResponseForbidden( + "You are not authorized to view this dashboard." + ) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -431,6 +409,7 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView): estimates = entity.get_estimates().count() context["dealer"] = dealer + context["total_activity"] = total_activity context["total_cars"] = total_cars context["total_reservations"] = total_reservations context["total_cost_price"] = total_cost_price @@ -475,6 +454,7 @@ class SalesDashboard(LoginRequiredMixin, TemplateView): the sales dashboard. :type template_name: str """ + template_name = "dashboards/sales.html" def get_context_data(self, **kwargs): @@ -486,11 +466,6 @@ class SalesDashboard(LoginRequiredMixin, TemplateView): reserved_by=self.request.user, reserved_until__gte=timezone.now() ).count() - # new_leads = models.Lead.objects.filter(dealer=dealer, status=models.Status.NEW).count() - pending_leads = models.Lead.objects.filter( - dealer=dealer, dealer__staff__assigned=staff, status=models.Status.PENDING - ).count() - # canceled_leads = models.Lead.objects.filter(dealer=dealer, status=models.Status.CANCELED).count() available_cars = models.Car.objects.filter( dealer=dealer, status=models.CarStatusChoices.AVAILABLE ).count() @@ -547,11 +522,11 @@ class WelcomeView(TemplateView): :ivar template_name: Path to the template used by the view. :type template_name: str """ + template_name = "welcome.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - dealer = get_user_type(self.request) plan_list = Plan.objects.all() context["plan_list"] = plan_list return context @@ -574,6 +549,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): :ivar permission_required: Permissions required to add a car. :type permission_required: list """ + model = models.Car form_class = forms.CarForm template_name = "inventory/car_form.html" @@ -605,6 +581,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): return context + def car_history(request, slug): """ Fetch and display the history of activities related to a specific car. @@ -643,6 +620,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): :ivar request: Django request object containing HTTP request details. """ + def get(self, request, *args, **kwargs): action = request.GET.get("action") handlers = { @@ -669,7 +647,8 @@ class AjaxHandlerView(LoginRequiredMixin, View): if not vin_no or len(vin_no.strip()) != 17: return JsonResponse( - {"success": False, "error": _("Invalid VIN number provided")}, status=400 + {"success": False, "error": _("Invalid VIN number provided")}, + status=400, ) vin_no = vin_no.strip() @@ -678,7 +657,8 @@ class AjaxHandlerView(LoginRequiredMixin, View): # manufacturer_name = model_name = year_model = None if not (result := decodevin(vin_no)): return JsonResponse( - {"success": False, "error": _("VIN not found in all sources")}, status=404 + {"success": False, "error": _("VIN not found in all sources")}, + status=404, ) manufacturer_name, model_name, year_model = result.values() @@ -691,7 +671,10 @@ class AjaxHandlerView(LoginRequiredMixin, View): if not car_make: return JsonResponse( - {"success": False, "error": _("Manufacturer not found in the database")}, + { + "success": False, + "error": _("Manufacturer not found in the database"), + }, status=404, ) vin_data["make_id"] = car_make.id_car_make @@ -730,7 +713,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): series = models.CarSerie.objects.filter(query).values( "id_car_serie", "name", "arabic_name", "generation_name" ) - except Exception as e: + except Exception: return JsonResponse({"error": _("Server error occurred")}, status=500) return JsonResponse(list(series), safe=False) @@ -818,18 +801,6 @@ class AjaxHandlerView(LoginRequiredMixin, View): return JsonResponse(serialized_options, safe=False) -import cv2 -import numpy as np -from pyzbar.pyzbar import decode -from django.views import View -from django.shortcuts import render, get_object_or_404, redirect -from django.http import JsonResponse -from django.contrib.auth.mixins import LoginRequiredMixin -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt -from django.urls import reverse -from . import models # Adjust to your project structure - @method_decorator(csrf_exempt, name="dispatch") class SearchCodeView(LoginRequiredMixin, View): template_name = "inventory/scan_vin.html" @@ -853,15 +824,19 @@ class SearchCodeView(LoginRequiredMixin, View): decoded_objects = decode(gray) if not decoded_objects: - return JsonResponse({"success": False, "error": _("No QR/Barcode detected")}) + return JsonResponse( + {"success": False, "error": _("No QR/Barcode detected")} + ) code = decoded_objects[0].data.decode("utf-8").strip() car = get_object_or_404(models.Car, vin=code) - return JsonResponse({ - "success": True, - "code": code, - "redirect_url": reverse("car_detail", args=[car.slug]) - }) + return JsonResponse( + { + "success": True, + "code": code, + "redirect_url": reverse("car_detail", args=[car.slug]), + } + ) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) @@ -894,6 +869,7 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: The permission(s) required to access this view. :type permission_required: list """ + model = models.Car home_label = _("inventory") template_name = "inventory/car_inventory.html" @@ -946,6 +922,7 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView): :ivar permission_required: List of permissions required by the view. :type permission_required: list """ + model = models.CarColors form_class = forms.CarColorsForm template_name = "inventory/add_colors.html" @@ -988,6 +965,7 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: The permission required to access this view. :type permission_required: str """ + model = models.Car template_name = "inventory/car_list_view.html" context_object_name = "cars" @@ -1025,7 +1003,6 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): ) return context - def get_queryset(self): dealer = get_user_type(self.request) qs = super().get_queryset() @@ -1062,7 +1039,6 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return qs - @login_required def inventory_stats_view(request): """ @@ -1167,7 +1143,7 @@ def inventory_stats_view(request): for make_data in inventory.values() ], } - print(result['makes']) + print(result["makes"]) return render(request, "inventory/inventory_stats.html", {"inventory": result}) @@ -1193,6 +1169,7 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): view. :type permission_required: list """ + model = models.Car template_name = "inventory/car_detail.html" context_object_name = "car" @@ -1218,6 +1195,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi :ivar permission_required: The list of permissions required to access this view. :type permission_required: list """ + model = models.CarFinance form_class = forms.CarFinanceForm template_name = "inventory/car_finance_form.html" @@ -1272,6 +1250,7 @@ class CarFinanceUpdateView( :ivar permission_required: List of permissions required to access the view. :type permission_required: list """ + model = models.CarFinance form_class = forms.CarFinanceForm template_name = "inventory/car_finance_form.html" @@ -1324,6 +1303,7 @@ class CarUpdateView( :ivar permission_required: List of permissions required to access this view. :type permission_required: list[str] """ + model = models.Car form_class = forms.CarUpdateForm template_name = "inventory/car_edit.html" @@ -1340,6 +1320,7 @@ class CarUpdateView( form.fields["vendor"].queryset = dealer.vendors.all() return form + class CarDeleteView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView ): @@ -1359,6 +1340,7 @@ class CarDeleteView( :ivar permission_required: A list of permission strings required to access this view. :type permission_required: list[str] """ + model = models.Car template_name = "inventory/car_confirm_delete.html" success_url = reverse_lazy("inventory_stats") @@ -1388,6 +1370,7 @@ class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV :ivar permission_required: List of permissions required to create a car location. :type permission_required: list """ + model = models.CarLocation form_class = forms.CarLocationForm template_name = "inventory/car_location_form.html" @@ -1424,6 +1407,7 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV :ivar permission_required: Permissions required to access this view. :type permission_required: list """ + model = models.CarLocation form_class = forms.CarLocationForm template_name = "inventory/car_location_form.html" @@ -1462,6 +1446,7 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView): :ivar template_name: The path to the template used to render the view. :type template_name: str """ + model = models.CarTransfer form_class = forms.CarTransferForm template_name = "inventory/car_transfer_form.html" @@ -1471,7 +1456,9 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView): form.fields["to_dealer"].queryset = models.Dealer.objects.exclude( pk=get_user_type(self.request).pk ).all() - form.fields["car"].queryset = models.Car.objects.filter(slug=self.kwargs["slug"]) + form.fields["car"].queryset = models.Car.objects.filter( + slug=self.kwargs["slug"] + ) return form def get_initial(self): @@ -1489,7 +1476,6 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView): return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug}) - class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView): """ Provides a detailed view of a specific car transfer record. @@ -1510,6 +1496,7 @@ class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView) to reference the car transfer record. :type context_object_name: str """ + model = models.CarTransfer template_name = "inventory/transfer_details.html" context_object_name = "transfer" @@ -1520,7 +1507,6 @@ class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView) return context - @login_required def car_transfer_approve(request, slug, transfer_pk): """ @@ -1652,6 +1638,7 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView): :ivar template_name: The name of the template used to render the view. :type template_name: str """ + model = models.CustomCard form_class = forms.CustomCardForm template_name = "inventory/add_custom_card.html" @@ -1693,6 +1680,7 @@ class CarRegistrationCreateView(LoginRequiredMixin, CreateView): registration form. :type template_name: str """ + model = models.CarRegistration form_class = forms.CarRegistrationForm template_name = "inventory/car_registration_form.html" @@ -1806,6 +1794,7 @@ class DealerDetailView(LoginRequiredMixin, DetailView): :ivar context_object_name: The name used to refer to the object in the template context. :type context_object_name: str """ + model = models.Dealer template_name = "dealers/dealer_detail.html" context_object_name = "dealer" @@ -1856,6 +1845,7 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): :ivar success_message: The message displayed upon a successful update. :type success_message: str """ + model = models.Dealer form_class = forms.DealerForm template_name = "dealers/dealer_form.html" @@ -1892,6 +1882,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: A list of permissions required to access the view. :type permission_required: list """ + model = models.Customer home_label = _("customers") context_object_name = "customers" @@ -1929,6 +1920,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView :ivar permission_required: The list of permissions required to access this view. :type permission_required: list[str] """ + model = models.Customer template_name = "customers/view_customer.html" context_object_name = "customer" @@ -2026,17 +2018,18 @@ def add_activity_to_customer(request, pk): class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ -# Handles the creation of a new customer within the system. This view ensures that proper permissions -# and request methods are utilized. It provides feedback to the user about the success or failure of -# the customer creation process. When the form is submitted and valid, it checks for duplicate -# customers based on the email provided before proceeding with the customer creation. + # Handles the creation of a new customer within the system. This view ensures that proper permissions + # and request methods are utilized. It provides feedback to the user about the success or failure of + # the customer creation process. When the form is submitted and valid, it checks for duplicate + # customers based on the email provided before proceeding with the customer creation. + + # :param request: The HTTP request object containing metadata about the request initiated by the user. + # :type request: HttpRequest + # :return: The rendered form page or a redirect to the customer list page upon successful creation. + # :rtype: HttpResponse + # :raises PermissionDenied: If the user does not have the required permissions to access the view. + #""" -# :param request: The HTTP request object containing metadata about the request initiated by the user. -# :type request: HttpRequest -# :return: The rendered form page or a redirect to the customer list page upon successful creation. -# :rtype: HttpResponse -# :raises PermissionDenied: If the user does not have the required permissions to access the view. -# """ model = models.Customer form_class = forms.CustomerForm permission_required = ["django_ledger.add_customermodel"] @@ -2045,6 +2038,21 @@ class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView success_message = "Customer created successfully" def form_valid(self, form): + if customer := models.Customer.objects.filter( + email=form.instance.email + ).first(): + if not customer.active: + messages.error( + self.request, + _( + "Customer Account with this email is Deactivated,Please Contact Admin" + ), + ) + else: + messages.error( + self.request, _("Customer with this email already exists") + ) + return redirect("customer_create") dealer = get_user_type(self.request) form.instance.dealer = dealer user = form.instance.create_user_model() @@ -2055,29 +2063,31 @@ class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView return super().form_valid(form) + class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ -# 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. + # 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 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 + # :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 + #""" -# :return: A rendered HTML template displaying the customer form pre-filled -# with existing data if a GET request is received. On successful form -# submission (POST request), redirects to the customer list page -# and displays a success message. In case of invalid data or errors, -# returns the rendered form template with the validation errors. -# :rtype: HttpResponse -# """ model = models.Customer form_class = forms.CustomerForm permission_required = ["django_ledger.change_customermodel"] @@ -2140,6 +2150,7 @@ class VendorListView(LoginRequiredMixin, ListView): ordered by their creation date in descending order. :type ordering: list """ + model = models.Vendor context_object_name = "vendors" paginate_by = 10 @@ -2148,7 +2159,7 @@ class VendorListView(LoginRequiredMixin, ListView): def get_queryset(self): query = self.request.GET.get("q") dealer = get_user_type(self.request) - vendors = super().get_queryset().filter(dealer=dealer,active=True) + vendors = super().get_queryset().filter(dealer=dealer, active=True) if query: return apply_search_filters(vendors, query) return vendors @@ -2199,6 +2210,7 @@ class VendorCreateView( :ivar success_message: The message displayed upon successful creation. :type success_message: str """ + model = models.Vendor form_class = forms.VendorForm template_name = "vendors/vendor_form.html" @@ -2206,9 +2218,14 @@ class VendorCreateView( success_message = _("Vendor created successfully") def form_valid(self, form): - if vendor:= models.Vendor.objects.filter(email=form.instance.email).first(): + if vendor := models.Vendor.objects.filter(email=form.instance.email).first(): if not vendor.active: - messages.error(self.request, _("Vendor Account with this email is Deactivated,Please Contact Admin")) + messages.error( + self.request, + _( + "Vendor Account with this email is Deactivated,Please Contact Admin" + ), + ) else: messages.error(self.request, _("Vendor with this email already exists")) return redirect("vendor_create") @@ -2244,6 +2261,7 @@ class VendorUpdateView( :ivar success_message: The message to display upon successful data update. :type success_message: str """ + model = models.Vendor form_class = forms.VendorForm template_name = "vendors/vendor_form.html" @@ -2316,6 +2334,7 @@ class GroupListView(LoginRequiredMixin, ListView): :ivar template_name: The path to the template used for rendering the group list. :type template_name: str """ + model = models.CustomGroup context_object_name = "groups" paginate_by = 10 @@ -2344,6 +2363,7 @@ class GroupDetailView(LoginRequiredMixin, DetailView): instance will be available in the template. :type context_object_name: str """ + model = models.CustomGroup template_name = "groups/group_detail.html" context_object_name = "group" @@ -2372,6 +2392,7 @@ class GroupCreateView( :ivar success_message: A message displayed upon successful creation of a group. :type success_message: str """ + model = models.CustomGroup form_class = forms.GroupForm template_name = "groups/group_form.html" @@ -2414,6 +2435,7 @@ class GroupUpdateView( :ivar success_message: Message displayed upon successful update of a group. :type success_message: str """ + model = models.CustomGroup form_class = forms.GroupForm template_name = "groups/group_form.html" @@ -2489,7 +2511,7 @@ def GroupPermissionView(request, pk): # Users @login_required -def UserGroupView(request, pk): +def UserGroupView(request, slug): """ Handles the assignment of user groups to a specific staff member. This view allows updating the groups a staff member belongs to via a form submission. @@ -2505,8 +2527,7 @@ def UserGroupView(request, pk): user detail page after successful submission for POST requests. :rtype: HttpResponse or HttpResponseRedirect """ - staff = get_object_or_404(models.Staff, pk=pk) - + staff = get_object_or_404(models.Staff, slug=slug) if request.method == "POST": form = forms.UserGroupForm(request.POST) groups = request.POST.getlist("name") @@ -2517,7 +2538,7 @@ def UserGroupView(request, pk): staff.add_group(cg.group) messages.success(request, _("Group added successfully")) - return redirect("user_detail", pk=staff.pk) + return redirect("user_detail", slug=staff.slug) form = forms.UserGroupForm(initial={"name": staff.groups}) form.fields["name"].queryset = models.CustomGroup.objects.filter( @@ -2545,6 +2566,7 @@ class UserListView(LoginRequiredMixin, ListView): page. :type template_name: str """ + model = models.Staff context_object_name = "users" paginate_by = 10 @@ -2553,7 +2575,7 @@ class UserListView(LoginRequiredMixin, ListView): def get_queryset(self): query = self.request.GET.get("q") dealer = get_user_type(self.request) - staff = models.Staff.objects.filter(dealer=dealer).all() + staff = models.Staff.objects.filter(dealer=dealer, active=True).all() return apply_search_filters(staff, query) @@ -2574,6 +2596,7 @@ class UserDetailView(LoginRequiredMixin, DetailView): :ivar context_object_name: Name of the context variable available in the template. :type context_object_name: str """ + model = models.Staff template_name = "users/user_detail.html" context_object_name = "user_" @@ -2606,6 +2629,7 @@ class UserCreateView( :ivar success_message: The success message displayed upon user creation. :type success_message: str """ + model = models.Staff form_class = forms.StaffForm template_name = "users/user_form.html" @@ -2622,7 +2646,12 @@ class UserCreateView( # return self.form_invalid(form) if dealer.is_staff_exceed_quota_limit: - messages.error(self.request, _("You have reached the maximum number of staff users allowed for your plan")) + messages.error( + self.request, + _( + "You have reached the maximum number of staff users allowed for your plan" + ), + ) return self.form_invalid(form) email = form.cleaned_data["email"] @@ -2672,6 +2701,7 @@ class UserUpdateView( :ivar success_message: Message displayed to the user after a successful update. :type success_message: str """ + model = models.Staff form_class = forms.StaffForm template_name = "users/user_form.html" @@ -2714,7 +2744,7 @@ class UserUpdateView( @login_required -def UserDeleteview(request, pk): +def UserDeleteview(request, slug): """ Deletes a user and its associated staff member from the database and redirects to the user list page. Displays a success message upon successful deletion @@ -2725,9 +2755,8 @@ def UserDeleteview(request, pk): :return: An HTTP redirect to the user list page. """ - staff = get_object_or_404(models.Staff, pk=pk) - staff.staff_member.delete() - staff.delete() + staff = get_object_or_404(models.Staff, slug=slug) + staff.deactivate_account() messages.success(request, _("User deleted successfully")) return redirect("user_list") @@ -2751,6 +2780,7 @@ class OrganizationListView(LoginRequiredMixin, ListView): :ivar paginate_by: The number of organizations displayed per page. :type paginate_by: int """ + model = models.Organization template_name = "organizations/organization_list.html" context_object_name = "organizations" @@ -2782,6 +2812,7 @@ class OrganizationDetailView(LoginRequiredMixin, DetailView): template for accessing the organization's data. :type context_object_name: str """ + model = models.Organization template_name = "organizations/organization_detail.html" context_object_name = "organization" @@ -2789,19 +2820,20 @@ class OrganizationDetailView(LoginRequiredMixin, DetailView): class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ -# Handles the creation of a new organization via a web form. This view allows the -# authenticated user to submit data for creating an organization. If a POST request -# is received, it validates the data, checks for duplicate organizations, and -# creates a customer linked to the organization, including its associated -# information such as address, phone number, and logo. Upon success, the user -# is redirected to the organization list, and a success message is displayed. + # Handles the creation of a new organization via a web form. This view allows the + # authenticated user to submit data for creating an organization. If a POST request + # is received, it validates the data, checks for duplicate organizations, and + # creates a customer linked to the organization, including its associated + # information such as address, phone number, and logo. Upon success, the user + # is redirected to the organization list, and a success message is displayed. + + # :param request: The HTTP request object containing data for creating an organization. + # :type request: HttpRequest + # :return: An HTTP response object rendering the organization create form page or + # redirecting the user after a successful creation. + # :rtype: HttpResponse + #""" -# :param request: The HTTP request object containing data for creating an organization. -# :type request: HttpRequest -# :return: An HTTP response object rendering the organization create form page or -# redirecting the user after a successful creation. -# :rtype: HttpResponse -# """ model = models.Organization form_class = forms.OrganizationForm permission_required = ["django_ledger.add_customermodel"] @@ -2810,6 +2842,21 @@ class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create success_message = "Organization created successfully" def form_valid(self, form): + if organization := models.Organization.objects.filter( + email=form.instance.email + ).first(): + if not organization.active: + messages.error( + self.request, + _( + "Organization Account with this email is Deactivated,Please Contact Admin" + ), + ) + else: + messages.error( + self.request, _("Organization with this email already exists") + ) + return redirect("organization_create") dealer = get_user_type(self.request) form.instance.dealer = dealer user = form.instance.create_user_model() @@ -2819,23 +2866,25 @@ class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create return super().form_valid(form) + class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ -# 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. + # 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 + #""" -# :param request: The HTTP request object. Must be authenticated via login. -# :type request: HttpRequest -# :param pk: The primary key of the organization to be updated. -# :type pk: int -# :return: An HTTP response object. Either renders the organization form or redirects -# to the organization list upon successful update. -# :rtype: HttpResponse -# """ model = models.Organization form_class = forms.OrganizationForm permission_required = ["django_ledger.change_customermodel"] @@ -2848,8 +2897,9 @@ class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update form.instance.update_customer_model() return super().form_valid(form) + @login_required -def OrganizationDeleteView(request, pk): +def OrganizationDeleteView(request, slug): """ Handles the deletion of an organization based on the provided primary key (pk). Looks up the organization and its corresponding user by email, attempts to delete both, and provides @@ -2863,7 +2913,7 @@ def OrganizationDeleteView(request, pk): :return: An HTTP response redirecting to the organization list view. :rtype: HttpResponseRedirect """ - organization = get_object_or_404(models.Organization, pk=pk) + organization = get_object_or_404(models.Organization, slug=slug) organization.deactivate_account() messages.success(request, _("Organization Deactivated successfully")) return redirect("organization_list") @@ -2888,6 +2938,7 @@ class RepresentativeListView(LoginRequiredMixin, ListView): :ivar paginate_by: The number of representatives displayed per page. :type paginate_by: int """ + model = models.Representative template_name = "representatives/representative_list.html" context_object_name = "representatives" @@ -2918,6 +2969,7 @@ class RepresentativeDetailView(LoginRequiredMixin, DetailView): the object. :type context_object_name: str """ + model = models.Representative template_name = "representatives/representative_detail.html" context_object_name = "representative" @@ -2943,6 +2995,7 @@ class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateVi :ivar success_message: The success message displayed after creating a representative. :type success_message: str """ + model = models.Representative form_class = forms.RepresentativeForm template_name = "representatives/representative_form.html" @@ -2985,6 +3038,7 @@ class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateVi operation. :type success_message: str """ + model = models.Representative form_class = forms.RepresentativeForm template_name = "representatives/representative_form.html" @@ -3010,6 +3064,7 @@ class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVi :ivar success_message: The success message displayed after a representative is deleted. :type success_message: str """ + model = models.Representative template_name = "representatives/representative_confirm_delete.html" success_url = reverse_lazy("representative_list") @@ -3040,6 +3095,7 @@ class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) :ivar permission_required: The required permissions to access the view. :type permission_required: list[str] """ + model = BankAccountModel template_name = "ledger/bank_accounts/bank_account_list.html" context_object_name = "bank_accounts" @@ -3080,12 +3136,13 @@ class BankAccountCreateView( :ivar permission_required: List of permissions required to access the view. :type permission_required: list """ + model = BankAccountModel form_class = BankAccountCreateForm template_name = "ledger/bank_accounts/bank_account_form.html" success_url = reverse_lazy("bank_account_list") success_message = _("Bank account created successfully") - permission_required = ['inventory.view_carfinance'] + permission_required = ["inventory.view_carfinance"] def form_valid(self, form): dealer = get_user_type(self.request) @@ -3120,6 +3177,7 @@ class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV :ivar permission_required: List of permissions required to access the view. :type permission_required: list[str] """ + model = BankAccountModel template_name = "ledger/bank_accounts/bank_account_detail.html" context_object_name = "bank_account" @@ -3151,12 +3209,13 @@ class BankAccountUpdateView( :ivar permission_required: List of permissions required to access the update view. :type permission_required: list """ + model = BankAccountModel form_class = BankAccountUpdateForm template_name = "ledger/bank_accounts/bank_account_form.html" success_url = reverse_lazy("bank_account_list") success_message = _("Bank account updated successfully") - permission_required = ['inventory.view_carfinance'] + permission_required = ["inventory.view_carfinance"] def get_form_kwargs(self): dealer = get_user_type(self.request) @@ -3226,6 +3285,7 @@ class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: Permissions required to access this view. :type permission_required: list[str] """ + model = AccountModel template_name = "ledger/coa_accounts/account_list.html" context_object_name = "accounts" @@ -3273,12 +3333,13 @@ class AccountCreateView( the permission checking to prevent unauthorized access. :type permission_required: list """ + model = AccountModel form_class = AccountModelCreateForm template_name = "ledger/coa_accounts/account_form.html" success_url = reverse_lazy("account_list") success_message = _("Account created successfully") - permission_required = ['inventory.view_carfinance'] + permission_required = ["inventory.view_carfinance"] def form_valid(self, form): dealer = get_user_type(self.request) @@ -3325,6 +3386,7 @@ class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) :ivar extra_context: Additional context data passed to the template. :type extra_context: dict """ + model = AccountModel template_name = "ledger/coa_accounts/account_detail.html" context_object_name = "account" @@ -3348,18 +3410,15 @@ class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) x.amount for x in account_model.transactionmodel_set.filter(tx_type="credit") ) - txs_qs = ( - account_model.transactionmodel_set.all() - .posted() - .order_by("journal_entry__timestamp") - .select_related( - "journal_entry", - "journal_entry__entity_unit", - "journal_entry__ledger__billmodel", - "journal_entry__ledger__invoicemodel", - ) - ) + account_model.transactionmodel_set.all().posted().order_by( + "journal_entry__timestamp" + ).select_related( + "journal_entry", + "journal_entry__entity_unit", + "journal_entry__ledger__billmodel", + "journal_entry__ledger__invoicemodel", + ) return context @@ -3388,12 +3447,13 @@ class AccountUpdateView( :ivar permission_required: List of permissions required to access the view. :type permission_required: list of str """ + model = AccountModel form_class = AccountModelUpdateForm template_name = "ledger/coa_accounts/account_form.html" success_url = reverse_lazy("account_list") success_message = _("Account updated successfully") - permission_required = ['inventory.view_carfinance'] + permission_required = ["inventory.view_carfinance"] def get_form(self, form_class=None): form = super().get_form(form_class) @@ -3446,7 +3506,7 @@ def sales_list_view(request): transactions = ItemTransactionModel.objects.for_entity( entity_slug=entity.slug, user_model=dealer.user - ).order_by('created') + ).order_by("created") paginator = Paginator(transactions, 10) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) @@ -3477,6 +3537,7 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: List of permissions required to view this page. :type permission_required: list """ + model = EstimateModel template_name = "sales/estimates/estimate_list.html" context_object_name = "estimates" @@ -3540,12 +3601,18 @@ def create_estimate(request, pk=None): if isinstance(quantities, list): if "0" in quantities: return JsonResponse( - {"status": "error", "message": _("Quantity must be greater than zero")} + { + "status": "error", + "message": _("Quantity must be greater than zero"), + } ) else: if int(quantities) <= 0: return JsonResponse( - {"status": "error", "message": _("Quantity must be greater than zero")} + { + "status": "error", + "message": _("Quantity must be greater than zero"), + } ) if isinstance(items, list): for item, quantity in zip(items, quantities): @@ -3554,7 +3621,12 @@ def create_estimate(request, pk=None): > models.Car.objects.filter(hash=item, status="available").count() ): return JsonResponse( - {"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" + ), + }, ) else: if ( @@ -3562,10 +3634,17 @@ def create_estimate(request, pk=None): > models.Car.objects.filter(hash=items, status="available").count() ): return JsonResponse( - {"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_title=title, customer_model=customer.customer_model, contract_terms="fixed" + estimate_title=title, + customer_model=customer.customer_model, + contract_terms="fixed", ) if isinstance(items, list): item_quantity_map = {} @@ -3642,7 +3721,7 @@ def create_estimate(request, pk=None): additioinal_info__car_info__hash=items ).first() instance = models.Car.objects.get(hash=item) - response = reserve_car(instance, request) + reserve_car(instance, request) opportunity_id = data.get("opportunity_id") if opportunity_id != "None": @@ -3670,8 +3749,29 @@ def create_estimate(request, pk=None): customer = opportunity.customer form.initial["customer"] = customer - car_list = models.Car.objects.filter(dealer=dealer,colors__isnull=False,finances__isnull=False,status="available").annotate(color=F('colors__exterior__rgb'),color_name=F('colors__exterior__arabic_name')).values_list( - 'id_car_make__arabic_name', 'id_car_model__arabic_name','id_car_serie__arabic_name','id_car_trim__arabic_name','color','color_name','hash').annotate(hash_count=Count('hash')).distinct() + car_list = ( + models.Car.objects.filter( + dealer=dealer, + colors__isnull=False, + finances__isnull=False, + status="available", + ) + .annotate( + color=F("colors__exterior__rgb"), + color_name=F("colors__exterior__arabic_name"), + ) + .values_list( + "id_car_make__arabic_name", + "id_car_model__arabic_name", + "id_car_serie__arabic_name", + "id_car_trim__arabic_name", + "color", + "color_name", + "hash", + ) + .annotate(hash_count=Count("hash")) + .distinct() + ) context = { "form": form, "items": [ @@ -3713,6 +3813,7 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView :ivar permission_required: List of permissions required to access the view. :type permission_required: list """ + model = EstimateModel template_name = "sales/estimates/estimate_detail.html" context_object_name = "estimate" @@ -3830,6 +3931,7 @@ class PaymentRequest(LoginRequiredMixin, PermissionRequiredMixin, DetailView): this view. The user must have the specified permissions. :type permission_required: list """ + model = EstimateModel template_name = "sales/estimates/payment_request_detail.html" context_object_name = "estimate" @@ -3864,6 +3966,7 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie :ivar permission_required: List of permissions required to access this view. :type permission_required: List[str] """ + model = EstimateModel context_object_name = "estimate" template_name = "sales/estimates/estimate_preview.html" @@ -3899,8 +4002,6 @@ def estimate_mark_as(request, pk): :rtype: HttpResponseRedirect """ estimate = get_object_or_404(EstimateModel, pk=pk) - dealer = get_user_type(request) - entity = dealer.entity mark = request.GET.get("mark") if mark: if mark == "review": @@ -3936,7 +4037,7 @@ def estimate_mark_as(request, pk): ) car.status = "available" car.save() - except Exception as e: + except Exception: pass messages.success(request, _("Quotation canceled successfully")) estimate.save() @@ -3967,6 +4068,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: List of permissions required for accessing this view. :type permission_required: list """ + model = InvoiceModel template_name = "sales/invoices/invoice_list.html" context_object_name = "invoices" @@ -3999,6 +4101,7 @@ class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) :ivar permission_required: List of permissions required to access the view. :type permission_required: list """ + model = InvoiceModel template_name = "sales/invoices/invoice_detail.html" context_object_name = "invoice" @@ -4046,6 +4149,7 @@ class DraftInvoiceModelUpdateFormView( this view. :type permission_required: List[str] """ + model = InvoiceModel form_class = DraftInvoiceModelUpdateForm template_name = "sales/invoices/draft_invoice_update.html" @@ -4087,6 +4191,7 @@ class ApprovedInvoiceModelUpdateFormView( set to ``django_ledger.view_invoicemodel`` by default. :type permission_required: list of str """ + model = InvoiceModel form_class = ApprovedInvoiceModelUpdateForm template_name = "sales/invoices/approved_invoice_update.html" @@ -4131,6 +4236,7 @@ class PaidInvoiceModelUpdateFormView( :ivar permission_required: List of permissions required to access the view. :type permission_required: list of str """ + model = InvoiceModel form_class = PaidInvoiceModelUpdateForm template_name = "sales/invoices/paid_invoice_update.html" @@ -4291,6 +4397,7 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView :ivar permission_required: List of permissions required to access the view. :type permission_required: list """ + model = InvoiceModel context_object_name = "invoice" template_name = "sales/invoices/invoice_preview.html" @@ -4354,10 +4461,10 @@ def PaymentCreateView(request, pk): if not model.is_approved(): model.mark_as_approved(user_model=entity.admin) if model.amount_paid == model.amount_due: - messages.error(request,_("fully paid")) + messages.error(request, _("fully paid")) return redirect(redirect_url, pk=model.pk) if model.amount_paid + amount > model.amount_due: - messages.error(request,_("Amount exceeds due amount")) + messages.error(request, _("Amount exceeds due amount")) return redirect(redirect_url, pk=model.pk) try: @@ -4507,6 +4614,7 @@ class UserActivityLogListView(LoginRequiredMixin, ListView): :ivar paginate_by: The number of logs displayed per page. :type paginate_by: int """ + model = models.UserActivityLog template_name = "dealers/activity_log.html" context_object_name = "logs" @@ -4518,9 +4626,11 @@ class UserActivityLogListView(LoginRequiredMixin, ListView): queryset = queryset.filter(user__email=self.request.GET["user"]) return queryset[:100] # will update later with better pagination + def lead_view(request): return render(request, "crm/leads/lead_view.html") + # CRM RELATED VIEWS class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ @@ -4543,6 +4653,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: List of permissions required to access the view. :type permission_required: list[str] """ + model = models.Lead template_name = "crm/leads/lead_list.html" context_object_name = "leads" @@ -4576,6 +4687,7 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): :ivar permission_required: List of permissions required to access this view. :type permission_required: list """ + model = models.Lead template_name = "crm/leads/lead_detail.html" permission_required = ["inventory.view_lead"] @@ -4658,9 +4770,10 @@ def lead_create(request): customer.save() instance.customer = customer - if instance.lead_type == "organization": - organization = models.Organization.objects.filter(email=instance.email) + organization = models.Organization.objects.filter( + email=instance.email + ) if not organization: organization = models.Organization( dealer=dealer, @@ -4678,7 +4791,9 @@ def lead_create(request): messages.success(request, _("Lead created successfully")) return redirect("lead_list") else: - messages.error(request, f"Lead was not created ... : {str(form.errors)}") + messages.error( + request, f"Lead was not created ... : {str(form.errors)}" + ) except Exception as e: messages.error(request, f"Lead was not created ... : {str(e)}") @@ -4696,32 +4811,44 @@ def lead_create(request): ) return render(request, "crm/leads/lead_form.html", {"form": form}) + def lead_tracking(request): dealer = get_user_type(request) new = models.Lead.objects.filter(dealer=dealer, status="new") - follow_up = models.Lead.objects.filter(dealer=dealer, next_action__in=["call", "meeting"]) + follow_up = models.Lead.objects.filter( + dealer=dealer, next_action__in=["call", "meeting"] + ) won = models.Lead.objects.filter(dealer=dealer, status="won") lose = models.Lead.objects.filter(dealer=dealer, status="lost") negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation") - context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation} + context = { + "new": new, + "follow_up": follow_up, + "won": won, + "lose": lose, + "negotiation": negotiation, + } return render(request, "crm/leads/lead_tracking.html", context) + # @require_POST def update_lead_actions(request): try: - lead_id = request.POST.get('lead_id') - current_action = request.POST.get('current_action') - next_action = request.POST.get('next_action') - next_action_date = request.POST.get('next_action_date',None) - action_notes = request.POST.get('action_notes', '') - # Validate required fields + lead_id = request.POST.get("lead_id") + current_action = request.POST.get("current_action") + next_action = request.POST.get("next_action") + next_action_date = request.POST.get("next_action_date", None) + print(request.POST) if not all([lead_id, current_action, next_action]): - return JsonResponse({'success': False, 'message': 'All fields are required'}, status=400) + return JsonResponse( + {"success": False, "message": "All fields are required"}, status=400 + ) # Get the lead lead = models.Lead.objects.get(id=lead_id) # Update lead fields + lead.status = current_action lead.next_action = next_action @@ -4729,20 +4856,27 @@ def update_lead_actions(request): try: if next_action_date: lead.next_action_date = next_action_date - next_action_datetime = datetime.strptime(next_action_date, '%Y-%m-%dT%H:%M') + next_action_datetime = datetime.strptime( + next_action_date, "%Y-%m-%dT%H:%M" + ) lead.next_action_date = timezone.make_aware(next_action_datetime) except ValueError: - return JsonResponse({'success': False, 'message': 'Invalid date format'}, status=400) + return JsonResponse( + {"success": False, "message": "Invalid date format"}, status=400 + ) # Save the lead lead.save() - return JsonResponse({'success': True, 'message': 'Actions updated successfully'}) + return JsonResponse( + {"success": True, "message": "Actions updated successfully"} + ) except models.Lead.DoesNotExist: - return JsonResponse({'success': False, 'message': 'Lead not found'}, status=404) + return JsonResponse({"success": False, "message": "Lead not found"}, status=404) except Exception as e: - return JsonResponse({'success': False, 'message': str(e)}, status=500) + return JsonResponse({"success": False, "message": str(e)}, status=500) + class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ @@ -4767,6 +4901,7 @@ class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): :ivar permission_required: List of permissions required for accessing this view. :type permission_required: list """ + model = models.Lead form_class = forms.LeadForm template_name = "crm/leads/lead_form.html" @@ -4858,49 +4993,13 @@ def add_note_to_opportunity(request, slug): if not notes: messages.error(request, _("Notes field is required")) else: - models.Notes.objects.create(content_object=opportunity, created_by=request.user,note=notes) + models.Notes.objects.create( + content_object=opportunity, created_by=request.user, note=notes + ) messages.success(request, _("Note added successfully")) return redirect("opportunity_detail", slug=opportunity.slug) -@login_required -def update_note(request, pk): - """ - Handles the update of a specific note attached to a lead object, ensuring that the user - making the request has permissions over the note. If the request method is `POST`, it - validates and updates the note using the submitted data. If validation is successful, - it redirects to the lead's detail page. Otherwise, a form is rendered for editing. - - :param request: The HTTP request object containing metadata about the request - and the user making the request. - :type request: HttpRequest - :param pk: The primary key of the note to be updated, identifying the specific - note object. - :type pk: int - :return: An HTTP response either rendering the note update form along with the - current note data, or a redirect to the lead detail page upon successful - update. - :rtype: HttpResponse - """ - note = get_object_or_404(models.Notes, pk=pk, created_by=request.user) - lead_pk = note.content_object.pk - lead = models.Lead.objects.get(pk=lead_pk) - - if request.method == "POST": - form = forms.NoteForm(request.POST, instance=note) - if form.is_valid(): - updated_note = form.save(commit=False) - updated_note.content_object = note.content_object - updated_note.created_by = request.user - updated_note.save() - messages.success(request, _("Note updated successfully")) - return redirect("lead_detail", slug=lead.slug) - else: - form = forms.NoteForm(instance=note) - - return render(request, "crm/note_form.html", {"form": form, "note": note}) - - @login_required def delete_note(request, pk): """ @@ -4949,7 +5048,14 @@ def lead_convert(request, slug): messages.error(request, _("Lead is already converted to customer")) else: customer = lead.convert_to_customer() - models.Opportunity.objects.create(dealer=dealer,customer=customer,lead=lead,probability=50,stage=models.Stage.NEGOTIATION,staff=lead.staff) + models.Opportunity.objects.create( + dealer=dealer, + customer=customer, + lead=lead, + probability=50, + stage=models.Stage.NEGOTIATION, + staff=lead.staff, + ) messages.success(request, _("Lead converted to customer successfully")) return redirect("lead_list") @@ -5019,7 +5125,9 @@ def schedule_lead(request, slug): ) instance.save() - messages.success(request, _("Lead scheduled and appointment created successfully")) + messages.success( + request, _("Lead scheduled and appointment created successfully") + ) return redirect("lead_list") else: messages.error(request, f"Invalid form data: {str(form.errors)}") @@ -5079,9 +5187,23 @@ def send_lead_email(request, slug, email_pk=None): lead = get_object_or_404(models.Lead, slug=slug) status = request.GET.get("status") dealer = get_user_type(request) - if status == 'draft': - models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.GET.get("to"), subject=request.GET.get("subject"), message=request.GET.get("message"),status=models.EmailStatus.DRAFT) - models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email Draft",created_by=request.user,activity_type=models.ActionChoices.EMAIL) + if status == "draft": + models.Email.objects.create( + content_object=lead, + created_by=request.user, + from_email="manager@tenhal.com", + to_email=request.GET.get("to"), + subject=request.GET.get("subject"), + message=request.GET.get("message"), + status=models.EmailStatus.DRAFT, + ) + models.Activity.objects.create( + dealer=dealer, + content_object=lead, + notes="Email Draft", + created_by=request.user, + activity_type=models.ActionChoices.EMAIL, + ) messages.success(request, _("Email Draft successfully")) response = HttpResponse(redirect("lead_detail", slug=lead.slug)) response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug]) @@ -5110,7 +5232,13 @@ def send_lead_email(request, slug, email_pk=None): request.POST.get("message"), ) dealer = get_user_type(request) - models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email sent",created_by=request.user,activity_type=models.ActionChoices.EMAIL) + models.Activity.objects.create( + dealer=dealer, + content_object=lead, + notes="Email sent", + created_by=request.user, + activity_type=models.ActionChoices.EMAIL, + ) messages.success(request, _("Email sent successfully")) return redirect("lead_list") msg = f""" @@ -5179,7 +5307,7 @@ def add_activity_to_lead(request, pk): return render(request, "crm/add_activity.html", {"form": form, "lead": lead}) -class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin): +class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): """ Handles the creation of Opportunity instances through a form while enforcing specific user access control and initial data population. This view ensures @@ -5198,6 +5326,7 @@ class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin): :ivar template_name: The template used to render the Opportunity creation form. :type template_name: str """ + model = models.Opportunity form_class = forms.OpportunityForm template_name = "crm/opportunities/opportunity_form.html" @@ -5207,9 +5336,9 @@ class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin): initial = super().get_initial() dealer = get_user_type(self.request) if self.kwargs.get("slug", None): - lead = models.Lead.objects.get(slug=self.kwargs.get("slug"),dealer=dealer) + lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer) initial["lead"] = lead - initial['stage'] = models.Stage.PROPOSAL + initial["stage"] = models.Stage.PROPOSAL return initial def form_valid(self, form): @@ -5223,7 +5352,7 @@ class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin): return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk}) -class OpportunityUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): +class OpportunityUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): """ Handles the update functionality for Opportunity objects. @@ -5245,6 +5374,7 @@ class OpportunityUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): update form. :type template_name: str """ + model = models.Opportunity form_class = forms.OpportunityForm template_name = "crm/opportunities/opportunity_form.html" @@ -5270,6 +5400,7 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView): :ivar context_object_name: The context variable name for the model object in the template. :type context_object_name: str """ + model = models.Opportunity template_name = "crm/opportunities/opportunity_detail.html" context_object_name = "opportunity" @@ -5309,35 +5440,38 @@ class OpportunityListView(LoginRequiredMixin, ListView): queryset = models.Opportunity.objects.filter(dealer=dealer) # Search filter - search = self.request.GET.get('search') + search = self.request.GET.get("search") if search: queryset = queryset.filter( - Q(customer__customer_name__icontains=search) | - Q(customer__email__icontains=search)) + Q(customer__customer_name__icontains=search) + | Q(customer__email__icontains=search) + ) # Stage filter - stage = self.request.GET.get('stage') + stage = self.request.GET.get("stage") if stage: queryset = queryset.filter(stage=stage) # Sorting - sort = self.request.GET.get('sort', 'newest') - if sort == 'newest': - queryset = queryset.order_by('-created') - elif sort == 'highest': - queryset = queryset.order_by('-expected_revenue') - elif sort == 'closing': - queryset = queryset.order_by('closing_date') + sort = self.request.GET.get("sort", "newest") + if sort == "newest": + queryset = queryset.order_by("-created") + elif sort == "highest": + queryset = queryset.order_by("-expected_revenue") + elif sort == "closing": + queryset = queryset.order_by("closing_date") return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['stage_choices'] = models.Stage.choices + context["stage_choices"] = models.Stage.choices return context def get_template_names(self): return self.template_name + + @login_required def delete_opportunity(request, pk): """ @@ -5392,9 +5526,9 @@ def opportunity_update_status(request, slug): if stage: opportunity.stage = stage opportunity.save() - messages.success(request,_("Opportunity status updated successfully")) - response = HttpResponse(redirect("opportunity_detail",slug=opportunity.slug)) - response['HX-Refresh'] = 'true' + messages.success(request, _("Opportunity status updated successfully")) + response = HttpResponse(redirect("opportunity_detail", slug=opportunity.slug)) + response["HX-Refresh"] = "true" return response @@ -5419,6 +5553,7 @@ class NotificationListView(LoginRequiredMixin, ListView): :ivar ordering: Defines the default ordering of notifications. :type ordering: str """ + model = models.Notification template_name = "crm/notifications_history.html" context_object_name = "notifications" @@ -5502,6 +5637,7 @@ class ItemServiceCreateView( :ivar permission_required: The permissions required for accessing this view. :type permission_required: list """ + model = models.AdditionalServices form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" @@ -5546,6 +5682,7 @@ class ItemServiceUpdateView( :ivar permission_required: Permissions required for accessing this view. :type permission_required: list[str] """ + model = models.AdditionalServices form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" @@ -5585,6 +5722,7 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) view. :type permission_required: list """ + model = models.AdditionalServices template_name = "items/service/service_list.html" context_object_name = "services" @@ -5616,6 +5754,7 @@ class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV :ivar permission_required: A list of permissions required to access the view. :type permission_required: list """ + model = ItemModel form_class = ExpenseItemCreateForm template_name = "items/expenses/expense_create.html" @@ -5655,6 +5794,7 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV :ivar permission_required: List of permissions required to access this view. :type permission_required: list[str] """ + model = ItemModel form_class = ExpenseItemUpdateForm template_name = "items/expenses/expense_update.html" @@ -5695,6 +5835,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) :ivar permission_required: The list of required permissions to access the view. :type permission_required: list[str] """ + model = ItemModel template_name = "items/expenses/expenses_list.html" context_object_name = "expenses" @@ -5724,6 +5865,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: List of permissions required to access the view. :type permission_required: list[str] """ + model = BillModel template_name = "ledger/bills/bill_list.html" context_object_name = "bills" @@ -5752,6 +5894,7 @@ class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): :ivar permission_required: A list of permissions required to access this view. :type permission_required: list[str] """ + model = BillModel template_name = "ledger/bills/bill_detail.html" context_object_name = "bill" @@ -5802,6 +5945,7 @@ class InReviewBillView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): :ivar permission_required: The list of permissions required to use this view. :type permission_required: list """ + model = BillModel form_class = InReviewBillModelUpdateForm template_name = "ledger/bills/bill_update_form.html" @@ -5854,6 +5998,7 @@ class ApprovedBillModelView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV :ivar permission_required: A list of permissions required to access this view. :type permission_required: list """ + model = BillModel form_class = ApprovedBillModelUpdateForm template_name = "ledger/bills/bill_update_form.html" @@ -5980,12 +6125,18 @@ def bill_create(request): if isinstance(quantities, list): if "0" in quantities: return JsonResponse( - {"status": "error", "message": _("Quantity must be greater than zero")} + { + "status": "error", + "message": _("Quantity must be greater than zero"), + } ) else: if int(quantities) <= 0: return JsonResponse( - {"status": "error", "message": _("Quantity must be greater than zero")} + { + "status": "error", + "message": _("Quantity must be greater than zero"), + } ) bill = entity.create_bill(vendor_model=vendor, terms=terms) @@ -6113,6 +6264,7 @@ class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): :ivar permission_required: A list of permissions required to access this view. :type permission_required: list[str] """ + model = models.SaleOrder template_name = "sales/orders/order_list.html" context_object_name = "orders" @@ -6267,6 +6419,7 @@ class BaseBalanceSheetRedirectView(RedirectView): :ivar url: The original URL or base URL to redirect users. :type url: str """ + def get_redirect_url(self, *args, **kwargs): year = get_localdate().year return reverse( @@ -6292,6 +6445,7 @@ class FiscalYearBalanceSheetViewBase( :ivar template_name: The template used for rendering the balance sheet view. :type template_name: str """ + template_name = "ledger/reports/balance_sheet.html" # AUTHORIZE_SUPERUSER = False # permission_required = [] @@ -6397,6 +6551,7 @@ class BaseIncomeStatementRedirectViewBase( :ivar request: The HTTP request object containing user session and other request-related data. :type request: HttpRequest """ + def get_redirect_url(self, *args, **kwargs): year = get_localdate().year dealer = get_user_type(self.request) @@ -6425,6 +6580,7 @@ class FiscalYearIncomeStatementViewBase( :ivar permission_required: List of permissions required to access the view. :type permission_required: list[str] """ + template_name = "ledger/reports/income_statement.html" permission_required = ["inventory.view_carfinance"] @@ -6526,6 +6682,7 @@ class BaseCashFlowStatementRedirectViewBase( :ivar request: The HTTP request object associated with the view. :type request: HttpRequest """ + def get_redirect_url(self, *args, **kwargs): year = get_localdate().year dealer = get_user_type(self.request) @@ -6555,6 +6712,7 @@ class FiscalYearCashFlowStatementViewBase( the view. :type permission_required: list """ + template_name = "ledger/reports/cash_flow_statement.html" permission_required = ["inventory.view_carfinance"] @@ -6645,6 +6803,7 @@ class EntityModelDetailHandlerViewBase( :ivar request: The HTTP request object associated with the current view. :type request: HttpRequest """ + def get_redirect_url(self, *args, **kwargs): loc_date = get_localdate() dealer = get_user_type(self.request) @@ -6687,6 +6846,7 @@ class EntityModelDetailBaseViewBase( dashboard. :type template_name: str """ + template_name = "ledger/reports/dashboard.html" def get_context_data(self, **kwargs): @@ -6834,6 +6994,7 @@ class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): :ivar http_method_names: HTTP methods supported by this view. :type http_method_names: list[str] """ + http_method_names = ["get"] def get(self, request, *args, **kwargs): @@ -6849,9 +7010,7 @@ class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): return JsonResponse({"results": net_payables}) - return JsonResponse({ - 'message': _('Unauthorized') - }, status=401) + return JsonResponse({"message": _("Unauthorized")}, status=401) class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): @@ -6867,6 +7026,7 @@ class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): :ivar http_method_names: Restricts the allowed HTTP methods for this view. :type http_method_names: list[str] """ + http_method_names = ["get"] def get(self, request, *args, **kwargs): @@ -6883,9 +7043,7 @@ class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): return JsonResponse({"results": net_receivable}) - return JsonResponse({ - 'message': _('Unauthorized') - }, status=401) + return JsonResponse({"message": _("Unauthorized")}, status=401) class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): @@ -6900,6 +7058,7 @@ class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): :ivar http_method_names: Restricts HTTP methods that can be used with this view. :type http_method_names: list[str] """ + http_method_names = ["get"] def get(self, request, *args, **kwargs): @@ -6943,9 +7102,7 @@ class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): return JsonResponse({"results": entity_pnl}) - return JsonResponse({ - 'message': _('Unauthorized') - }, status=401) + return JsonResponse({"message": _("Unauthorized")}, status=401) class EmployeeCalendarView(LoginRequiredMixin, ListView): @@ -6965,13 +7122,13 @@ class EmployeeCalendarView(LoginRequiredMixin, ListView): list of appointments in the template. :type context_object_name: str """ + template_name = "crm/employee_calendar.html" model = Appointment context_object_name = "appointments" def get_queryset(self): query = self.request.GET.get("q") - dealer = get_user_type(self.request) staff = getattr(self.request, "staff", None) if staff: appointments = Appointment.objects.filter( @@ -7033,6 +7190,7 @@ class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView): :ivar template_name: Defines the template to be used for rendering the view. :type template_name: str """ + model = models.Car table_class = tables.CarTable template_name = "inventory/car_list_table.html" @@ -7070,8 +7228,8 @@ def DealerSettingsView(request, slug): instance = form.save(commit=False) instance.dealer = dealer instance.save() - messages.success(request, _('Settings updated')) - return redirect('dealer_detail', slug=dealer.slug) + messages.success(request, _("Settings updated")) + return redirect("dealer_detail", slug=dealer.slug) else: print(form.errors) form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer}) @@ -7197,6 +7355,7 @@ class LedgerModelListView(LoginRequiredMixin, ListView, ArchiveIndexView): :ivar allow_empty: Allows rendering of the page even if the queryset is empty. :type allow_empty: bool """ + model = LedgerModel context_object_name = "ledgers" template_name = "ledger/ledger/ledger_list.html" @@ -7246,10 +7405,12 @@ class LedgerModelDetailView(LoginRequiredMixin, DetailView): :ivar template_name: The path to the template used to render the detailed ledger view. :type template_name: str """ + model = LedgerModel context_object_name = "ledger" template_name = "ledger/ledger/ledger_detail.html" + class LedgerModelCreateView(LedgerModelCreateViewBase): """ Handles the creation of LedgerModel entities. @@ -7263,6 +7424,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase): form rendering. :type template_name: str """ + template_name = "ledger/ledger/ledger_form.html" def get_form(self, form_class=None): @@ -7270,8 +7432,9 @@ class LedgerModelCreateView(LedgerModelCreateViewBase): return LedgerModelCreateForm( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) + def form_valid(self, form): dealer = get_user_type(self.request) instance = form.save(commit=False) @@ -7279,7 +7442,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase): return super().form_valid(form) def get_success_url(self): - return reverse('ledger_list') + return reverse("ledger_list") class LedgerModelModelActionView(LedgerModelModelActionViewBase): @@ -7295,6 +7458,7 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase): :ivar model: The model associated with this view. :type model: type[Model] """ + def get_redirect_url(self, *args, **kwargs): return reverse("ledger_list") @@ -7314,6 +7478,7 @@ class LedgerModelDeleteView(LedgerModelDeleteViewBase, SuccessMessageMixin): of the ledger instance. :type success_message: str """ + template_name = "ledger/ledger/ledger_delete.html" success_message = "Ledger deleted" @@ -7360,6 +7525,7 @@ class JournalEntryListView(LoginRequiredMixin, ListView): :ivar template_name: The path to the HTML template that renders the list view. :type template_name: str """ + model = JournalEntryModel context_object_name = "journal_entries" template_name = "ledger/journal_entry/journal_entry_list.html" @@ -7396,6 +7562,7 @@ class JournalEntryCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView :ivar success_message: The message displayed upon successfully creating a journal entry. :type success_message: str """ + model = JournalEntryModel template_name = "ledger/journal_entry/journal_entry_form.html" form_class = forms.JournalEntryModelCreateForm @@ -7492,6 +7659,7 @@ class JournalEntryModelTXSDetailView(JournalEntryModelTXSDetailViewBase): entry transactions view. :type template_name: str """ + template_name = "ledger/journal_entry/journal_entry_txs.html" @@ -7615,7 +7783,8 @@ def ledger_unpost_all_journals(request, entity_slug, pk): def pricing_page(request): plan_list = PlanPricing.objects.all() form = forms.PaymentPlanForm() - return render(request, "pricing_page.html", {"plan_list": plan_list, "form":form}) + return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) + # @require_POST def submit_plan(request): @@ -7630,20 +7799,23 @@ def submit_plan(request): amount=pp.price, currency=settings.DEFAULT_CURRENCY, tax=15, - status=AbstractOrder.STATUS.NEW + status=AbstractOrder.STATUS.NEW, ) - transaction_url = handle_payment(request,order) + transaction_url = handle_payment(request, order) return redirect(transaction_url) + def payment_callback(request): dealer = get_user_type(request) payment_id = request.GET.get("id") history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() payment_status = request.GET.get("status") - order = Order.objects.filter(user=dealer.user,status=AbstractOrder.STATUS.NEW).first() + order = Order.objects.filter( + user=dealer.user, status=AbstractOrder.STATUS.NEW + ).first() if payment_status == "paid": - billing_info,created = BillingInfo.objects.get_or_create( + billing_info, created = BillingInfo.objects.get_or_create( user=dealer.user, tax_number=dealer.vrn, name=dealer.arabic_name, @@ -7653,10 +7825,10 @@ def payment_callback(request): country=dealer.entity.country if dealer.entity.country else " ", ) if created: - userplan =UserPlan.objects.create( - user=request.user, - plan=order.plan, - active=True, + userplan = UserPlan.objects.create( + user=request.user, + plan=order.plan, + active=True, ) userplan.initialize() @@ -7664,12 +7836,14 @@ def payment_callback(request): history.status = "paid" history.save() invoice = order.get_invoices().first() - return render(request, "payment_success.html",{"order":order,"invoice":invoice}) + return render( + request, "payment_success.html", {"order": order, "invoice": invoice} + ) elif payment_status == "failed": history.status = "failed" history.save() - message = request.GET.get('message') + message = request.GET.get("message") return render(request, "payment_failed.html", {"message": message}) @@ -7680,27 +7854,26 @@ def task_list(request): # Add pagination paginator = Paginator(tasks, 10) # Show 10 tasks per page - page_number = request.GET.get('page') + page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) - return render(request, 'tasks/task_list.html', {'page_obj': page_obj}) + return render(request, "tasks/task_list.html", {"page_obj": page_obj}) + def sse_stream(request): def event_stream(): - last_id = request.GET.get('last_id', 0) + last_id = request.GET.get("last_id", 0) while True: # Check for new notifications notifications = models.Notification.objects.filter( - user=request.user, - id__gt=last_id, - is_read=False - ).order_by('created') + user=request.user, id__gt=last_id, is_read=False + ).order_by("created") for notification in notifications: notification_data = { - 'id': notification.id, - 'message': notification.message, - 'created': notification.created.isoformat(), - } + "id": notification.id, + "message": notification.message, + "created": notification.created.isoformat(), + } yield ( f"id: {notification.id}\n" @@ -7710,39 +7883,46 @@ def sse_stream(request): last_id = notification.id sleep(2) - response = StreamingHttpResponse( - event_stream(), - content_type='text/event-stream' - ) - response['Cache-Control'] = 'no-cache' + + response = StreamingHttpResponse(event_stream(), content_type="text/event-stream") + response["Cache-Control"] = "no-cache" return response @login_required def fetch_notifications(request): notifications = models.Notification.objects.filter( - user=request.user, - is_read=False - ).order_by('-created')[:10] # Get 10 most recent + user=request.user, is_read=False + ).order_by("-created")[:10] # Get 10 most recent + + return JsonResponse({"notifications": list(notifications.values())}) - return JsonResponse({'notifications': list(notifications.values())}) @login_required def mark_notification_as_read(request, notification_id): - notification = get_object_or_404(models.Notification, id=notification_id, user=request.user) + notification = get_object_or_404( + models.Notification, id=notification_id, user=request.user + ) notification.read = True notification.save() - return JsonResponse({'status': 'success'}) + return JsonResponse({"status": "success"}) + @login_required def mark_all_notifications_as_read(request): - models.Notification.objects.filter(user=request.user, is_read=False).update(read=True) - return JsonResponse({'status': 'success'}) + models.Notification.objects.filter(user=request.user, is_read=False).update( + read=True + ) + return JsonResponse({"status": "success"}) + @login_required def notifications_history(request): - models.Notification.objects.filter(user=request.user, is_read=False).update(read=True) - return JsonResponse({'status': 'success'}) + models.Notification.objects.filter(user=request.user, is_read=False).update( + read=True + ) + return JsonResponse({"status": "success"}) + # def activity_create(request,pk): # lead = get_object_or_404(models.Lead, pk=pk) @@ -7760,9 +7940,10 @@ def notifications_history(request): # ) # return render(request, 'activity_history.html') -def add_activity(request,content_type,slug): + +def add_activity(request, content_type, slug): try: - model = apps.get_model(f'inventory.{content_type}') + model = apps.get_model(f"inventory.{content_type}") except LookupError: raise Http404("Model not found") @@ -7775,8 +7956,8 @@ def add_activity(request,content_type,slug): activity.dealer = dealer activity.content_object = obj activity.created_by = request.user - activity.notes = form.cleaned_data['notes'] - activity.activity_type = form.cleaned_data['activity_type'] + activity.notes = form.cleaned_data["notes"] + activity.activity_type = form.cleaned_data["activity_type"] activity.save() messages.success(request, _("Activity added successfully")) @@ -7784,9 +7965,10 @@ def add_activity(request,content_type,slug): messages.error(request, _("Activity form is not valid")) return redirect(f"{content_type}_detail", slug=slug) -def add_task(request,content_type,slug): + +def add_task(request, content_type, slug): try: - model = apps.get_model(f'inventory.{content_type}') + model = apps.get_model(f"inventory.{content_type}") except LookupError: raise Http404("Model not found") @@ -7800,7 +7982,7 @@ def add_task(request,content_type,slug): task.content_object = obj task.assigned_to = request.user task.created_by = request.user - task.due_date = form.cleaned_data['due_date'] + task.due_date = form.cleaned_data["due_date"] task.save() messages.success(request, _("Task added successfully")) else: @@ -7808,7 +7990,8 @@ def add_task(request,content_type,slug): messages.error(request, _("Task form is not valid")) return redirect(f"{content_type}_detail", slug=slug) -def update_task(request,pk): + +def update_task(request, pk): task = get_object_or_404(models.Tasks, pk=pk) lead = get_object_or_404(models.Lead, pk=task.content_object.id) @@ -7821,14 +8004,13 @@ def update_task(request,pk): # response = HttpResponse() # response['HX-Refresh'] = 'true' # return response - tasks = models.Tasks.objects.filter( - content_type__model="lead", object_id=lead.id - ) - return render(request,'crm/leads/lead_detail.html',{'lead':lead,'tasks':tasks}) + tasks = models.Tasks.objects.filter(content_type__model="lead", object_id=lead.id) + return render(request, "crm/leads/lead_detail.html", {"lead": lead, "tasks": tasks}) -def add_note(request,content_type,slug): + +def add_note(request, content_type, slug): try: - model = apps.get_model(f'inventory.{content_type}') + model = apps.get_model(f"inventory.{content_type}") except LookupError: raise Http404("Model not found") @@ -7849,18 +8031,76 @@ def add_note(request,content_type,slug): messages.error(request, _("Note form is not valid")) return redirect(f"{content_type}_detail", slug=slug) -def update_note(request,pk): + +def update_note(request, pk): note = get_object_or_404(models.Notes, pk=pk) lead = get_object_or_404(models.Lead, pk=note.content_object.id) dealer = get_user_type(request) if request.method == "POST": - note.note = request.POST.get('note') + note.note = request.POST.get("note") note.save() messages.success(request, _("Note updated successfully")) - return redirect(f"lead_detail", slug=lead.slug) + return redirect("lead_detail", slug=lead.slug) else: messages.error(request, _("Note form is not valid")) notes = models.Notes.objects.filter( - content_type__model="lead", object_id=lead.id,dealer=dealer - ) - return render(request,'crm/leads/lead_detail.html',{'lead':lead,'notes':notes}) \ No newline at end of file + content_type__model="lead", object_id=lead.id, dealer=dealer + ) + return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes}) + + +# Admin Management + + +def management_view(request): + return render(request, "admin_management/management.html") + + +def user_management(request): + context = { + "customers": models.Customer.objects.filter(active=False), + "organizations": models.Organization.objects.filter(active=False), + "vendors": models.Vendor.objects.filter(active=False), + "staff": models.Staff.objects.filter(active=False), + } + return render(request, "admin_management/user_management.html", context) + + +def activate_account(request, content_type, slug): + try: + model = apps.get_model(f"inventory.{content_type}") + except LookupError: + raise Http404("Model not found") + + obj = get_object_or_404(model, slug=slug) + if request.method == "POST": + obj.activate_account() + messages.success(request, _("Account activated successfully")) + return redirect("user_management") + return render( + request, "admin_management/confirm_activate_account.html", {"obj": obj} + ) + + +def permenant_delete_account(request, content_type, slug): + try: + model = apps.get_model(f"inventory.{content_type}") + except LookupError: + raise Http404("Model not found") + + obj = get_object_or_404(model, slug=slug) + if request.method == "POST": + try: + obj.permenant_delete() + messages.success(request, _("Account Deleted successfully")) + except RestrictedError: + messages.error( + request, + _("You cannot delete this account,it is related to another account"), + ) + except Exception as e: + messages.error(request, _(f"Error deleting account: {e}")) + return redirect("user_management") + return render( + request, "admin_management/permenant_delete_account.html", {"obj": obj} + ) diff --git a/merge_db.py b/merge_db.py index cbdd5070..934d148c 100644 --- a/merge_db.py +++ b/merge_db.py @@ -1,4 +1,3 @@ -import pymysql from sqlalchemy import create_engine import pandas as pd diff --git a/scripts/new_wmis.py b/scripts/new_wmis.py index 5b9e01a1..27a6112d 100644 --- a/scripts/new_wmis.py +++ b/scripts/new_wmis.py @@ -1,6 +1,5 @@ from vininfo.utils import merge_wmi -from vininfo.dicts import WMI wmi_manufacturer_mapping = { diff --git a/scripts/r.py b/scripts/r.py index 24e25071..ce7ded46 100644 --- a/scripts/r.py +++ b/scripts/r.py @@ -1,10 +1,5 @@ -import json -import requests -from django.urls import reverse -from django.conf import settings from django.contrib.auth.models import User -from inventory.models import PaymentHistory,Notification -from plans.models import Order, PlanPricing,AbstractOrder +from inventory.models import Notification def run(): user = User.objects.first() diff --git a/scripts/run.py b/scripts/run.py index 7a4c5db5..e8f1b5d7 100644 --- a/scripts/run.py +++ b/scripts/run.py @@ -1,41 +1,13 @@ -from django_ledger.forms.account import AccountModelUpdateForm, AccountModelCreateForm -import requests -import os from dotenv import load_dotenv -from django.contrib.auth.models import Permission -from django.contrib.auth.models import Group -from django_ledger.models.invoice import InvoiceModel -from django_ledger.utils import accruable_net_summary -from decimal import Decimal from django_ledger.models import ( - EstimateModel, EntityModel, - ItemModel, - ItemTransactionModel, AccountModel, - CustomerModel, - EntityManagementModel, ) # from rich import print -from datetime import date from inventory.models import ( - Car, - Dealer, - VatRate, - Lead, CarMake, - CarModel, - Schedule, - CustomGroup, ) -from inventory.utils import CarFinanceCalculator -from appointment.models import Appointment, AppointmentRequest, Service, StaffMember -from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from django.contrib.auth import get_user_model -from django_ledger.io.io_core import get_localdate -from datetime import datetime, timedelta -from django.utils import timezone -import hashlib from django_ledger.io import roles User = get_user_model() diff --git a/scripts/run1.py b/scripts/run1.py index ed0be9b3..10d22438 100644 --- a/scripts/run1.py +++ b/scripts/run1.py @@ -1,23 +1,8 @@ -from django_ledger.forms.account import AccountModelUpdateForm,AccountModelCreateForm -import requests -import os from dotenv import load_dotenv -from django.contrib.auth.models import Permission -from django.contrib.auth.models import Group -from django_ledger.models.invoice import InvoiceModel -from django_ledger.utils import accruable_net_summary -from decimal import Decimal -from django_ledger.models import EstimateModel,EntityModel,ItemModel,ItemTransactionModel,AccountModel,CustomerModel,EntityManagementModel +from django_ledger.models import EntityModel # from rich import print -from datetime import date -from inventory.models import Car, Dealer, VatRate,Lead,CarMake,CarModel,Schedule,CustomGroup -from inventory.utils import CarFinanceCalculator -from appointment.models import Appointment,AppointmentRequest,Service,StaffMember +from inventory.models import CarMake from django.contrib.auth import get_user_model -from django_ledger.io.io_core import get_localdate -from datetime import datetime, timedelta -from django.utils import timezone -import hashlib from django_ledger.io import roles User = get_user_model() diff --git a/scripts/run2.py b/scripts/run2.py index bf1a5f85..82833dfb 100644 --- a/scripts/run2.py +++ b/scripts/run2.py @@ -1,24 +1,9 @@ -from django_ledger.forms.account import AccountModelUpdateForm,AccountModelCreateForm -import requests -import os from dotenv import load_dotenv -from django.contrib.auth.models import Permission -from django.contrib.auth.models import Group from django_ledger.models.invoice import InvoiceModel -from django_ledger.utils import accruable_net_summary -from decimal import Decimal -from django_ledger.models import EstimateModel,EntityModel,ItemModel,ItemTransactionModel,AccountModel,CustomerModel,EntityManagementModel from rich import print -from datetime import date -from inventory.models import Car, Dealer, VatRate,Lead,CarMake,CarModel,Schedule,CustomGroup +from inventory.models import Car from inventory.utils import CarFinanceCalculator -from appointment.models import Appointment,AppointmentRequest,Service,StaffMember from django.contrib.auth import get_user_model -from django_ledger.io.io_core import get_localdate -from datetime import datetime, timedelta -from django.utils import timezone -import hashlib -from django_ledger.io import roles User = get_user_model() diff --git a/templates/admin_management/confirm_activate_account.html b/templates/admin_management/confirm_activate_account.html new file mode 100644 index 00000000..63d7ea14 --- /dev/null +++ b/templates/admin_management/confirm_activate_account.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Activate Account

+

Are you sure you want to activate this account "{{ obj.email }}"?

+
+ {% csrf_token %} +
+ + Cancel +
+
+
+
+{% endblock %} diff --git a/templates/admin_management/management.html b/templates/admin_management/management.html new file mode 100644 index 00000000..3ce8a5b5 --- /dev/null +++ b/templates/admin_management/management.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block content %} +

Admin Management

+ +{% endblock content %} \ No newline at end of file diff --git a/templates/admin_management/permenant_delete_account.html b/templates/admin_management/permenant_delete_account.html new file mode 100644 index 00000000..8bea3f05 --- /dev/null +++ b/templates/admin_management/permenant_delete_account.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Delete Account

+

Are you sure you want to delete this account "{{ obj.email }}"? This will delete all associated information for this user.

+
+ {% csrf_token %} +
+ + Cancel +
+
+
+
+{% endblock %} + diff --git a/templates/admin_management/user_management.html b/templates/admin_management/user_management.html new file mode 100644 index 00000000..565c5bf5 --- /dev/null +++ b/templates/admin_management/user_management.html @@ -0,0 +1,243 @@ +{% extends 'base.html' %} +{% load i18n static humanize %} + +{% block title %} + {% trans 'User Management' %} +{% endblock title %} + +{% block content %} +
+
+

{% trans 'User Management' %}

+
+
+

{% trans 'Customers' %}

+
+ + + + + + + + + + + + + {% for customer in customers %} + + + + + + + + + {% empty %} + + {% endfor %} + +
{{ _('First Name') }}{{ _('Last Name') }}{{ _('Email') }}{{ _('Status') }}{{ _('Created date') }}{{ _('Actions') }}
{{ customer.first_name }}{{ customer.last_name }}{{ customer.email }} + {% if customer.active %} + {{ _('Active') }} + {% else %} + {{ _('Inactive') }} + {% endif %} + {{ customer.created|naturalday|capfirst }} + +
{% trans 'No data available in table' %}
+
+
+
+
+
+

{% trans 'Organizations' %}

+
+ + + + + + + + + + + + + {% for organization in organizations %} + + + + + + + + + {% empty %} + + {% endfor %} + +
{{ _('Name') }}{{ _('Arabic Name') }}{{ _('Email') }}{{ _('Status') }}{{ _('Create date') }}{{ _('Actions') }}
{{ organization.name }}{{ organization.arabic_name }}{{ organization.email }} + {% if customer.active %} + {{ _('Active') }} + {% else %} + {{ _('Inactive') }} + {% endif %} + {{ organization.created|naturalday|capfirst }} + +
{% trans 'No data available in table' %}
+
+
+
+
+
+

{% trans 'Vendors' %}

+
+ + + + + + + + + + + + + {% for vendor in vendors %} + + + + + + + + + {% empty %} + + {% endfor %} + +
{{ _('Name') }}{{ _('Arabic Name') }}{{ _('Email') }}{{ _('Status') }}{{ _('Create date') }}{{ _('Actions') }}
{{ vendor.name }}{{ vendor.arabic_name }}{{ vendor.email }} + {% if customer.active %} + {{ _('Active') }} + {% else %} + {{ _('Inactive') }} + {% endif %} + {{ vendor.created_at|naturalday|capfirst }} + +
{% trans 'No data available in table' %}
+
+
+
+
+
+

{% trans 'Staff' %}

+
+ + + + + + + + + + + + + {% for obj in staff %} + + + + + + + + + {% empty %} + + {% endfor %} + +
{{ _('Name') }}{{ _('Arabic Name') }}{{ _('Email') }}{{ _('Status') }}{{ _('Create date') }}{{ _('Actions') }}
{{ obj.name }}{{ obj.arabic_name }}{{ obj.email }} + {% if obj.active %} + {{ _('Active') }} + {% else %} + {{ _('Inactive') }} + {% endif %} + {{ obj.created|naturalday|capfirst }} + +
{% trans 'No data available in table' %}
+
+
+
+
+
+{% endblock %} + diff --git a/templates/crm/leads/lead_detail.html b/templates/crm/leads/lead_detail.html index 49ee9ca1..0e3aac82 100644 --- a/templates/crm/leads/lead_detail.html +++ b/templates/crm/leads/lead_detail.html @@ -130,14 +130,16 @@
{{ _("Status")}} {% if lead.status == "new" %} {{_("New")}} - {% elif lead.status == "pending" %} - {{_("Pending")}} - {% elif lead.status == "in_progress" %} - {{_("In Progress")}} - {% elif lead.status == "qualified" %} - {{_("Qualified")}} - {% elif lead.status == "canceled" %} - {{_("Canceled")}} + {% elif lead.status == "follow_up" %} + {{_("Follow Up")}} + {% elif lead.status == "negotiation" %} + {{_("Negotiation")}} + {% elif lead.status == "won" %} + {{_("Won")}} + {% elif lead.status == "lost" %} + {{_("Lost")}} + {% elif lead.status == "closed" %} + {{_("Closed")}} {% endif %}
@@ -158,51 +160,51 @@
-
{{ _("Email") }}
+
{{ _("Email") }}
- {{ lead.email }} + {{ lead.email }}
-
{{ _("Phone") }}
+
{{ _("Phone") }}
- {{ lead.phone_number}} + {{ lead.phone_number}}
-
-
{{ _("Salary")}}
+
{{CURRENCY}}  +
{{ _("Salary")}}
-

{{lead.salary}}

+

{{CURRENCY}} {{lead.salary}}

-
{{ _("Created")}}
+
{{ _("Created")}}
-

{{ lead.created|naturalday|capfirst }}

+ {{ lead.created|naturalday|capfirst }}
-
{{ _("Lead Source")}}
+
{{ _("Lead Source")}}
-

{{ lead.source|upper }}

+ {{ lead.source|upper }}
-
{{ _("Lead Channel")}}
+
{{ _("Lead Channel")}}
-

{{ lead.channel|upper }}

+ {{ lead.channel|upper }}
-
{{ _("Address") }}
+
{{ _("Address") }}
-

{{ lead.address}}

+ {{ lead.address}}
-
{{ _("City") }}
+
{{ _("City") }}
-

{{ lead.city }}

+ {{ lead.city }}
@@ -213,7 +215,7 @@
{{lead.status|capfirst}}
  {% trans "Current Stage" %}
-
{{lead.next_action|capfirst}}
  {% trans "Next Action" %}
+
{{lead.next_action|capfirst}}
  {% trans "Next Action" %} :  {{lead.next_action_date|naturalday|capfirst}}