diff --git a/.gitignore b/.gitignore index 8231ff2f..eb7342e3 100644 --- a/.gitignore +++ b/.gitignore @@ -163,8 +163,11 @@ GitHub.sublime-settings .history +static-copy static +static/* staticfiles media tmp -logs \ No newline at end of file +logs +static/testdir \ No newline at end of file diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 6dd9d7fa..08aa0cd7 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -17,40 +17,24 @@ import django django.setup() - from django.urls import path from channels.routing import ProtocolTypeRouter, URLRouter -from whitenoise import WhiteNoise from channels.auth import AuthMiddlewareStack -from api import routing from inventory.notifications.sse import NotificationSSEApp from django.urls import re_path from django.core.asgi import get_asgi_application -from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler -from pathlib import Path -# application = ProtocolTypeRouter( -# { -# "http": get_asgi_application(), -# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), -# } -# ) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") -django.setup() -BASE_DIR = Path(__file__).resolve().parent.parent + +# BASE_DIR = Path(__file__).resolve().parent.parent app = get_asgi_application() -# app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles')) - application = ProtocolTypeRouter( { "http": AuthMiddlewareStack( URLRouter( [ path("sse/notifications/", NotificationSSEApp()), - re_path( - r"", app - ), # All other routes go to Django + re_path(r"", app), ] ) ), @@ -58,5 +42,5 @@ application = ProtocolTypeRouter( ) -if django.conf.settings.DEBUG: - application = ASGIStaticFilesHandler(app) \ No newline at end of file +# if django.conf.settings.DEBUG: +# application = ASGIStaticFilesHandler(app) diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 0f1085b0..c89e3c75 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -5,7 +5,7 @@ from django.urls import path, include from schema_graph.views import Schema from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns -from inventory.notifications.sse import NotificationSSEApp +# from inventory.notifications.sse import NotificationSSEApp # import debug_toolbar # from two_factor.urls import urlpatterns as tf_urls @@ -33,5 +33,6 @@ urlpatterns += i18n_patterns( # path('', include(tf_urls)), ) -# if not settings.DEBUG: -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/inventory/admin.py b/inventory/admin.py index d952714d..25940cfb 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -3,6 +3,7 @@ from django.contrib import admin from . import models from django_ledger import models as ledger_models from django.contrib import messages + # from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait # from appointment import models as appointment_models from import_export.admin import ExportMixin @@ -177,55 +178,52 @@ class CarOptionAdmin(admin.ModelAdmin): # actions = [export_to_pdf_landscape, export_to_pdf_portrait] - @admin.register(models.UserRegistration) class UserRegistrationAdmin(admin.ModelAdmin): # Fields to display in the list view list_display = [ - 'name', - 'arabic_name', - 'email', - 'crn', - 'vrn', - 'phone_number', - 'is_created', - 'created_at', + "name", + "arabic_name", + "email", + "crn", + "vrn", + "phone_number", + "is_created", + "created_at", ] # Filters in the right sidebar list_filter = [ - 'is_created', - 'created_at', + "is_created", + "created_at", ] # Searchable fields - search_fields = [ - 'name', 'arabic_name', 'email', 'crn', 'vrn', 'phone_number' - ] + search_fields = ["name", "arabic_name", "email", "crn", "vrn", "phone_number"] # Read-only fields in detail view - readonly_fields = [ - 'created_at', 'updated_at', 'is_created', 'password' - ] + readonly_fields = ["created_at", "updated_at", "is_created", "password"] # Organize form layout fieldsets = [ - ('Account Information', { - 'fields': ('name', 'arabic_name', 'email', 'phone_number') - }), - ('Business Details', { - 'fields': ('crn', 'vrn', 'address') - }), - ('Status', { - 'fields': ('is_created', 'password', 'created_at', 'updated_at'), - 'classes': ('collapse',) - }), + ( + "Account Information", + {"fields": ("name", "arabic_name", "email", "phone_number")}, + ), + ("Business Details", {"fields": ("crn", "vrn", "address")}), + ( + "Status", + { + "fields": ("is_created", "password", "created_at", "updated_at"), + "classes": ("collapse",), + }, + ), ] # Custom action to create accounts - actions = ['create_dealer_accounts'] + actions = ["create_dealer_accounts"] - @admin.action(description='Create dealer account(s) for selected registrations') + @admin.action(description="Create dealer account(s) for selected registrations") def create_dealer_accounts(self, request, queryset): created_count = 0 already_created_count = 0 @@ -242,7 +240,7 @@ class UserRegistrationAdmin(admin.ModelAdmin): self.message_user( request, f"Error creating account for {registration.name}: {str(e)}", - level=messages.ERROR + level=messages.ERROR, ) failed_count += 1 @@ -251,17 +249,17 @@ class UserRegistrationAdmin(admin.ModelAdmin): self.message_user( request, f"Successfully created {created_count} account(s).", - level=messages.SUCCESS + level=messages.SUCCESS, ) if already_created_count > 0: self.message_user( request, f"{already_created_count} registration(s) were already created.", - level=messages.INFO + level=messages.INFO, ) if failed_count > 0: self.message_user( request, f"Failed to create {failed_count} account(s). Check logs.", - level=messages.ERROR - ) \ No newline at end of file + level=messages.ERROR, + ) diff --git a/inventory/forms.py b/inventory/forms.py index d5a34498..1d928a94 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -2,6 +2,7 @@ from django.core.cache import cache from datetime import datetime from luhnchecker.luhn import Luhn from django.contrib.auth.models import Permission + # from appointment.models import Service from django.core.validators import MinLengthValidator from django import forms @@ -57,7 +58,7 @@ from .models import ( Tasks, Recall, Ticket, - UserRegistration + UserRegistration, ) from django_ledger import models as ledger_models from django.forms import ( @@ -364,7 +365,14 @@ class CarForm( "receiving_date", "vendor", ] - required_fields = ["vin","id_car_make", "id_car_model", "id_car_serie", "id_car_trim", "vendor"] + required_fields = [ + "vin", + "id_car_make", + "id_car_model", + "id_car_serie", + "id_car_trim", + "vendor", + ] widgets = { "id_car_make": forms.Select(attrs={"class": "form-select form-select-sm"}), "receiving_date": forms.DateTimeInput(attrs={"type": "datetime-local"}), @@ -2123,8 +2131,7 @@ class AdditionalFinancesForm(forms.Form): for field in self.fields.values(): if isinstance(field, forms.ModelMultipleChoiceField): field.widget.choices = [ - (obj.pk, f"{obj.name} - {obj.price:.2f}") - for obj in field.queryset + (obj.pk, f"{obj.name} - {obj.price:.2f}") for obj in field.queryset ] @@ -2141,6 +2148,7 @@ class VatRateForm(forms.ModelForm): model = VatRate fields = ["rate"] + class CustomSetPasswordForm(SetPasswordForm): new_password1 = forms.CharField( label="New Password", @@ -2258,18 +2266,26 @@ class TicketResolutionForm(forms.ModelForm): self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")] - class CarDealershipRegistrationForm(forms.ModelForm): # Add additional fields for the registration form class Meta: model = UserRegistration - fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address") + fields = ( + "name", + "arabic_name", + "email", + "phone_number", + "crn", + "vrn", + "address", + ) + class CarDetailsEstimateCreate(forms.Form): customer = forms.ModelChoiceField( - queryset=Customer.objects.all(), + queryset=Customer.objects.filter(active=True), required=True, label="Customer", widget=forms.Select(attrs={"class": "form-control"}), - ) \ No newline at end of file + ) diff --git a/inventory/hooks.py b/inventory/hooks.py index b972523c..c53894dc 100644 --- a/inventory/hooks.py +++ b/inventory/hooks.py @@ -18,10 +18,10 @@ def check_create_coa_accounts(task): logger.warning("Account creation task failed, checking status...") try: - dealer_id = task.kwargs.get('dealer_id',None) - coa_slug = task.kwargs.get('coa_slug', None) + dealer_id = task.kwargs.get("dealer_id", None) + coa_slug = task.kwargs.get("coa_slug", None) logger.info(f"Checking accounts for dealer {dealer_id}") - logger.info(f"COA slug: {coa_slug}") + logger.info(f"COA slug: {coa_slug}") if not dealer_id: logger.error("No dealer_id in task kwargs") return @@ -37,7 +37,9 @@ def check_create_coa_accounts(task): try: coa = entity.get_coa_model_qs().get(slug=coa_slug) except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") + logger.error( + f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}" + ) else: coa = entity.get_default_coa() if not coa: @@ -49,7 +51,11 @@ def check_create_coa_accounts(task): missing_accounts = [] for account_data in get_accounts_data(): - if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists(): + if ( + not entity.get_all_accounts() + .filter(coa_model=coa, code=account_data["code"]) + .exists() + ): missing_accounts.append(account_data) logger.info(f"Missing account: {account_data['code']}") @@ -62,6 +68,8 @@ def check_create_coa_accounts(task): except Exception as e: logger.error(f"Error in check_create_coa_accounts hook: {e}") + + # def check_create_coa_accounts(task): # logger.info("Checking if all accounts are created") # instance = task.kwargs["dealer"] diff --git a/inventory/management/commands/deactivate_unsubscribed_dealers.py b/inventory/management/commands/deactivate_unsubscribed_dealers.py index 47f4d960..e3de9066 100644 --- a/inventory/management/commands/deactivate_unsubscribed_dealers.py +++ b/inventory/management/commands/deactivate_unsubscribed_dealers.py @@ -8,19 +8,23 @@ from django.core.management.base import BaseCommand User = get_user_model() + class Command(BaseCommand): help = "Deactivates expired user plans" def handle(self, *args, **options): users_without_plan = User.objects.filter( - is_active=True, userplan=None, dealer__isnull=False, date_joined__lte=timezone.now()-timedelta(days=7) + is_active=True, + userplan=None, + dealer__isnull=False, + date_joined__lte=timezone.now() - timedelta(days=7), ) count = users_without_plan.count() for user in users_without_plan: user.is_active = False user.save() - subject = 'Your account has been deactivated' + subject = "Your account has been deactivated" message = """ Hello {},\n Your account has been deactivated, please contact us at {} if you have any questions. @@ -30,7 +34,7 @@ class Command(BaseCommand): """.format(user.dealer.name, settings.DEFAULT_FROM_EMAIL) from_email = settings.DEFAULT_FROM_EMAIL recipient_list = user.email - send_email(from_email, recipient_list,subject, message) + send_email(from_email, recipient_list, subject, message) self.stdout.write( self.style.SUCCESS( diff --git a/inventory/management/commands/plans_maintenance.py b/inventory/management/commands/plans_maintenance.py index c7222f36..726b406f 100644 --- a/inventory/management/commands/plans_maintenance.py +++ b/inventory/management/commands/plans_maintenance.py @@ -63,14 +63,14 @@ class Command(BaseCommand): for plan in expired_plans: # try: - if dealer := getattr(plan.user,"dealer", None): + if dealer := getattr(plan.user, "dealer", None): dealer.user.is_active = False dealer.user.save() for staff in dealer.get_staff(): staff.deactivate_account() count = expired_plans.update(active=False) - # except: - # logger.warning(f"User {plan.user_id} does not exist") + # except: + # logger.warning(f"User {plan.user_id} does not exist") self.stdout.write(f"Deactivated {count} expired plans") def cleanup_old_orders(self): diff --git a/inventory/management/commands/set_custom_permissions.py b/inventory/management/commands/set_custom_permissions.py index 4fffa995..c74db16f 100644 --- a/inventory/management/commands/set_custom_permissions.py +++ b/inventory/management/commands/set_custom_permissions.py @@ -7,16 +7,16 @@ from django_ledger.models import EstimateModel, BillModel, AccountModel, LedgerM class Command(BaseCommand): def handle(self, *args, **kwargs): - Permission.objects.get_or_create( - name="Can view crm", - codename="can_view_crm", - content_type=ContentType.objects.get_for_model(Lead), - ) - Permission.objects.get_or_create( - name="Can reassign lead", - codename="can_reassign_lead", - content_type=ContentType.objects.get_for_model(Lead), - ) + # Permission.objects.get_or_create( + # name="Can view crm", + # codename="can_view_crm", + # content_type=ContentType.objects.get_for_model(Lead), + # ) + # Permission.objects.get_or_create( + # name="Can reassign lead", + # codename="can_reassign_lead", + # content_type=ContentType.objects.get_for_model(Lead), + # ) Permission.objects.get_or_create( name="Can view sales", codename="can_view_sales", @@ -47,4 +47,3 @@ class Command(BaseCommand): codename="can_approve_estimatemodel", content_type=ContentType.objects.get_for_model(EstimateModel), ) - diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index 8d21a127..89589f53 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -23,19 +23,19 @@ class Command(BaseCommand): # Note: Deleting plans and quotas should cascade to related objects like PlanQuota and PlanPricing. self.stdout.write(self.style.SUCCESS("Data reset complete.")) else: - self.stdout.write(self.style.NOTICE("Creating or updating default plans and quotas...")) + self.stdout.write( + self.style.NOTICE("Creating or updating default plans and quotas...") + ) # Create or get quotas users_quota, created_u = Quota.objects.get_or_create( - codename="Users", - defaults={"name": "Users", "unit": "number"} + codename="Users", defaults={"name": "Users", "unit": "number"} ) if created_u: self.stdout.write(self.style.SUCCESS('Created quota: "Users"')) cars_quota, created_c = Quota.objects.get_or_create( - codename="Cars", - defaults={"name": "Cars", "unit": "number"} + codename="Cars", defaults={"name": "Cars", "unit": "number"} ) if created_c: self.stdout.write(self.style.SUCCESS('Created quota: "Cars"')) @@ -43,90 +43,81 @@ class Command(BaseCommand): # Create or get plans basic_plan, created_bp = Plan.objects.get_or_create( name="Basic", - defaults={"description": "basic plan", "available": True, "visible": True} + defaults={"description": "basic plan", "available": True, "visible": True}, ) if created_bp: self.stdout.write(self.style.SUCCESS('Created plan: "Basic"')) pro_plan, created_pp = Plan.objects.get_or_create( name="Pro", - defaults={"description": "Pro plan", "available": True, "visible": True} + defaults={"description": "Pro plan", "available": True, "visible": True}, ) if created_pp: self.stdout.write(self.style.SUCCESS('Created plan: "Pro"')) enterprise_plan, created_ep = Plan.objects.get_or_create( name="Enterprise", - defaults={"description": "Enterprise plan", "available": True, "visible": True} + defaults={ + "description": "Enterprise plan", + "available": True, + "visible": True, + }, ) if created_ep: self.stdout.write(self.style.SUCCESS('Created plan: "Enterprise"')) # Assign quotas to plans using get_or_create to prevent duplicates PlanQuota.objects.get_or_create( - plan=basic_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=basic_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=basic_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=basic_plan, quota=cars_quota, defaults={"value": 10000000} ) - + # Pro plan quotas PlanQuota.objects.get_or_create( - plan=pro_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=pro_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=pro_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=pro_plan, quota=cars_quota, defaults={"value": 10000000} ) # Enterprise plan quotas PlanQuota.objects.get_or_create( - plan=enterprise_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=enterprise_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=enterprise_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=enterprise_plan, quota=cars_quota, defaults={"value": 10000000} ) # Create or get pricing basic_pricing, created_bp_p = Pricing.objects.get_or_create( - name="3 Months", - defaults={"period": 90} + name="3 Months", defaults={"period": 90} ) pro_pricing, created_pp_p = Pricing.objects.get_or_create( - name="6 Months", - defaults={"period": 180} + name="6 Months", defaults={"period": 180} ) enterprise_pricing, created_ep_p = Pricing.objects.get_or_create( - name="1 Year", - defaults={"period": 365} + name="1 Year", defaults={"period": 365} ) # Assign pricing to plans PlanPricing.objects.get_or_create( plan=basic_plan, pricing=basic_pricing, - defaults={"price": Decimal("2997.00")} + defaults={"price": Decimal("2997.00")}, ) PlanPricing.objects.get_or_create( - plan=pro_plan, - pricing=pro_pricing, - defaults={"price": Decimal("5395.00")} + plan=pro_plan, pricing=pro_pricing, defaults={"price": Decimal("5395.00")} ) PlanPricing.objects.get_or_create( plan=enterprise_plan, pricing=enterprise_pricing, - defaults={"price": Decimal("9590.00")} + defaults={"price": Decimal("9590.00")}, ) - self.stdout.write(self.style.SUCCESS("Subscription plans structure successfully created or updated.")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + "Subscription plans structure successfully created or updated." + ) + ) diff --git a/inventory/management/commands/update_site.py b/inventory/management/commands/update_site.py index 9e000038..9fc9a7ed 100644 --- a/inventory/management/commands/update_site.py +++ b/inventory/management/commands/update_site.py @@ -3,12 +3,13 @@ from django.core.management.base import BaseCommand from django.contrib.sites.models import Site from django.conf import settings + class Command(BaseCommand): - help = 'Update the default site domain' + help = "Update the default site domain" def handle(self, *args, **options): site = Site.objects.get_current() site.domain = settings.SITE_DOMAIN site.name = settings.SITE_NAME site.save() - self.stdout.write(self.style.SUCCESS(f'Site updated to: {site.domain}')) \ No newline at end of file + self.stdout.write(self.style.SUCCESS(f"Site updated to: {site.domain}")) diff --git a/inventory/models.py b/inventory/models.py index 556529c3..b2b2e1d8 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -10,7 +10,7 @@ from django.urls import reverse # from django.utils.text import slugify from slugify import slugify from django.utils import timezone -from django.core.validators import MinValueValidator,MaxValueValidator +from django.core.validators import MinValueValidator, MaxValueValidator import hashlib from django.db import models from datetime import timedelta @@ -57,15 +57,17 @@ from encrypted_model_fields.fields import ( EncryptedEmailField, EncryptedTextField, ) + # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords from plans.models import Invoice -from django_extensions.db.fields import RandomCharField,AutoSlugField +from django_extensions.db.fields import RandomCharField, AutoSlugField logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) + class Base(models.Model): id = models.UUIDField( unique=True, @@ -206,11 +208,8 @@ class VatRate(models.Model): max_digits=5, decimal_places=2, default=Decimal("0.15"), - validators=[ - MinValueValidator(0.0), - MaxValueValidator(1.0) - ], - help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)") + validators=[MinValueValidator(0.0), MaxValueValidator(1.0)], + help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"), ) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) @@ -776,7 +775,7 @@ class Car(Base): make = self.id_car_make.name if self.id_car_make else "Unknown Make" model = self.id_car_model.name if self.id_car_model else "Unknown Model" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" - vin=self.vin if self.vin else None + vin = self.vin if self.vin else None return f"{self.year} - {make} - {model} - {trim}-{vin}" @property @@ -843,7 +842,7 @@ class Car(Base): def mark_as_sold(self): self.cancel_reservation() self.status = CarStatusChoices.SOLD - self.sold_date=timezone.now() + self.sold_date = timezone.now() self.save() def cancel_reservation(self): @@ -904,16 +903,23 @@ class Car(Base): def get_active_estimates(self): try: - qs = self.item_model.itemtransactionmodel_set.exclude(ce_model__status="canceled") + qs = self.item_model.itemtransactionmodel_set.exclude( + ce_model__status="canceled" + ) data = [] for item in qs: - x = ExtraInfo.objects.filter(object_id=item.ce_model.pk,content_type=ContentType.objects.get_for_model(EstimateModel)).first() + x = ExtraInfo.objects.filter( + object_id=item.ce_model.pk, + content_type=ContentType.objects.get_for_model(EstimateModel), + ).first() if x: data.append(x) return data except Exception as e: - logger.error(f"Error getting active estimates for car {self.vin} error: {e}") + logger.error( + f"Error getting active estimates for car {self.vin} error: {e}" + ) return [] @property @@ -1389,7 +1395,11 @@ class Dealer(models.Model, LocalizedNameMixin): options={"quality": 80}, ) entity = models.ForeignKey( - EntityModel, on_delete=models.SET_NULL, null=True, blank=True,related_name="dealers" + EntityModel, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="dealers", ) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @@ -1421,6 +1431,7 @@ class Dealer(models.Model, LocalizedNameMixin): except Exception as e: print(e) return None + @property def is_plan_expired(self): try: @@ -1455,6 +1466,7 @@ class Dealer(models.Model, LocalizedNameMixin): def get_vendors(self): return VendorModel.objects.filter(entity_model=self.entity) + def get_staff(self): return Staff.objects.filter(dealer=self) @@ -1505,7 +1517,9 @@ class Staff(models.Model): first_name = models.CharField(max_length=255, verbose_name=_("First Name")) last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"),null=True,blank=True) + arabic_name = models.CharField( + max_length=255, verbose_name=_("Arabic Name"), null=True, blank=True + ) phone_number = EncryptedCharField( max_length=255, verbose_name=_("Phone Number"), @@ -1794,7 +1808,7 @@ class Customer(models.Model): commit=False, customer_model_kwargs={ "customer_name": self.full_name, - "address_1": "",#self.address, + "address_1": "", # self.address, # "phone": self.phone_number, # "email": self.email, }, @@ -2120,6 +2134,10 @@ class Lead(models.Model): slug = RandomCharField(length=8, unique=True) class Meta: + permissions = [ + ("can_view_crm", _("Can view CRM")), + ("can_reassign_lead", _("Can reassign lead")), + ] verbose_name = _("Lead") verbose_name_plural = _("Leads") indexes = [ @@ -2305,10 +2323,14 @@ class Schedule(models.Model): help_text=_("What is the status of this schedule?"), ) created_at = models.DateTimeField( - auto_now_add=True, verbose_name=_("Created Date"), help_text=_("When was this schedule created?") + auto_now_add=True, + verbose_name=_("Created Date"), + help_text=_("When was this schedule created?"), ) updated_at = models.DateTimeField( - auto_now=True, verbose_name=_("Updated Date"), help_text=_("When was this schedule last updated?") + auto_now=True, + verbose_name=_("Updated Date"), + help_text=_("When was this schedule last updated?"), ) def __str__(self): @@ -2495,13 +2517,12 @@ class Opportunity(models.Model): def __str__(self): try: if self.customer: - return ( - f"Opportunity for {self.customer.first_name} {self.customer.last_name}" - ) + return f"Opportunity for {self.customer.first_name} {self.customer.last_name}" return f"Opportunity for {self.organization.name}" except Exception: return f"Opportunity for car :{self.car}" + class Notes(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes") content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -2765,7 +2786,6 @@ class Vendor(models.Model, LocalizedNameMixin): ), ] - def __str__(self): return self.name @@ -3155,7 +3175,7 @@ class CustomGroup(models.Model): "notes", "tasks", "activity", - "additionalservices" + "additionalservices", ], ) self.set_permissions( @@ -3272,7 +3292,7 @@ class CustomGroup(models.Model): "payment", "vendor", "additionalservices", - 'customer' + "customer", ], other_perms=[ "view_car", @@ -3340,7 +3360,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Cash account to track cash transactions when an invoice is created."), + help_text=_( + "Cash account to track cash transactions when an invoice is created." + ), verbose_name=_("Invoice Cash Account"), ) invoice_prepaid_account = models.ForeignKey( @@ -3349,7 +3371,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Prepaid Revenue account to track prepaid revenue when an invoice is created."), + help_text=_( + "Prepaid Revenue account to track prepaid revenue when an invoice is created." + ), verbose_name=_("Invoice Prepaid Account"), ) invoice_unearned_account = models.ForeignKey( @@ -3358,7 +3382,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Unearned Revenue account to track unearned revenue when an invoice is created."), + help_text=_( + "Unearned Revenue account to track unearned revenue when an invoice is created." + ), verbose_name=_("Invoice Unearned Account"), ) invoice_tax_payable_account = models.ForeignKey( @@ -3367,7 +3393,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Tax Payable account to track tax liabilities when an invoice is created."), + help_text=_( + "Tax Payable account to track tax liabilities when an invoice is created." + ), verbose_name=_("Invoice Tax Payable Account"), ) invoice_vehicle_sale_account = models.ForeignKey( @@ -3376,7 +3404,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Vehicle Sales account to track vehicle sales revenue when an invoice is created."), + help_text=_( + "Vehicle Sales account to track vehicle sales revenue when an invoice is created." + ), verbose_name=_("Invoice Vehicle Sale Account"), ) invoice_additional_services_account = models.ForeignKey( @@ -3385,7 +3415,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Additional Services account to track additional services revenue when an invoice is created."), + help_text=_( + "Additional Services account to track additional services revenue when an invoice is created." + ), verbose_name=_("Invoice Additional Services Account"), ) invoice_cost_of_good_sold_account = models.ForeignKey( @@ -3394,7 +3426,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Cost of Goods Sold account to track the cost of goods sold when an invoice is created."), + help_text=_( + "Cost of Goods Sold account to track the cost of goods sold when an invoice is created." + ), verbose_name=_("Invoice Cost of Goods Sold Account"), ) @@ -3404,7 +3438,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Inventory account to track the cost of goods sold when an invoice is created."), + help_text=_( + "Inventory account to track the cost of goods sold when an invoice is created." + ), verbose_name=_("Invoice Inventory Account"), ) @@ -3423,7 +3459,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Prepaid account to track prepaid expenses when a bill is created."), + help_text=_( + "Prepaid account to track prepaid expenses when a bill is created." + ), verbose_name=_("Bill Prepaid Account"), ) bill_unearned_account = models.ForeignKey( @@ -3432,10 +3470,14 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Unearned account to track unearned expenses when a bill is created."), + help_text=_( + "Unearned account to track unearned expenses when a bill is created." + ), verbose_name=_("Bill Unearned Account"), ) - additional_info = models.JSONField(default=dict, null=True, blank=True, help_text=_("Additional information")) + additional_info = models.JSONField( + default=dict, null=True, blank=True, help_text=_("Additional information") + ) def __str__(self): return f"Settings for {self.dealer}" @@ -3790,7 +3832,10 @@ class Ticket(models.Model): ] dealer = models.ForeignKey( - Dealer, on_delete=models.CASCADE, related_name="tickets", verbose_name=_("Dealer") + Dealer, + on_delete=models.CASCADE, + related_name="tickets", + verbose_name=_("Dealer"), ) subject = models.CharField( max_length=200, verbose_name=_("Subject"), help_text=_("Short description") @@ -3882,7 +3927,6 @@ class CarImage(models.Model): ) - class UserRegistration(models.Model): name = models.CharField(_("Name"), max_length=255) arabic_name = models.CharField(_("Arabic Name"), max_length=255) @@ -3892,15 +3936,24 @@ class UserRegistration(models.Model): verbose_name=_("Phone Number"), validators=[SaudiPhoneNumberValidator()], ) - crn = models.CharField(_("Commercial Registration Number"), max_length=10, unique=True) + crn = models.CharField( + _("Commercial Registration Number"), max_length=10, unique=True + ) vrn = models.CharField(_("Vehicle Registration Number"), max_length=15, unique=True) address = models.TextField(_("Address")) - password = models.CharField(_("Password"), max_length=255,null=True,blank=True) + password = models.CharField(_("Password"), max_length=255, null=True, blank=True) is_created = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - REQUIRED_FIELDS = ["username", "arabic_name", "crn", "vrn", "address", "phone_number"] + REQUIRED_FIELDS = [ + "username", + "arabic_name", + "crn", + "vrn", + "address", + "phone_number", + ] def __str__(self): return self.email @@ -3924,7 +3977,7 @@ class UserRegistration(models.Model): phone=self.phone_number, crn=self.crn, vrn=self.vrn, - address=self.address + address=self.address, ) if dealer: @@ -3939,4 +3992,4 @@ class UserRegistration(models.Model): except Exception as e: logger.error(f"Error creating account for {self.email}: {e}") - return False \ No newline at end of file + return False diff --git a/inventory/notifications/sse.py b/inventory/notifications/sse.py index dd525d14..c00b0ae1 100644 --- a/inventory/notifications/sse.py +++ b/inventory/notifications/sse.py @@ -1,82 +1,3 @@ -# import json -# from django.contrib.auth.models import AnonymousUser -# from django.contrib.auth import get_user_model -# from django.db import close_old_connections -# from urllib.parse import parse_qs -# from channels.db import database_sync_to_async -# from inventory.models import Notification -# import asyncio - -# @database_sync_to_async -# def get_notifications(user, last_id): -# return Notification.objects.filter( -# user=user, id__gt=last_id, is_read=False -# ).order_by("created") - -# class NotificationSSEApp: -# async def __call__(self, scope, receive, send): -# if scope["type"] != "http": -# return - -# query_string = parse_qs(scope["query_string"].decode()) -# last_id = int(query_string.get("last_id", [0])[0]) - -# # Get user from scope if using AuthMiddlewareStack -# user = scope.get("user", AnonymousUser()) -# if not user.is_authenticated: -# await send({ -# "type": "http.response.start", -# "status": 403, -# "headers": [(b"content-type", b"text/plain")], -# }) -# await send({ -# "type": "http.response.body", -# "body": b"Unauthorized", -# }) -# return - -# await send({ -# "type": "http.response.start", -# "status": 200, -# "headers": [ -# (b"content-type", b"text/event-stream"), -# (b"cache-control", b"no-cache"), -# (b"x-accel-buffering", b"no"), -# ] -# }) - -# try: -# while True: -# close_old_connections() - -# notifications = await get_notifications(user, last_id) -# for notification in notifications: -# data = { -# "id": notification.id, -# "message": notification.message, -# "created": notification.created.isoformat(), -# "is_read": notification.is_read, -# } - -# event_str = ( -# f"id: {notification.id}\n" -# f"event: notification\n" -# f"data: {json.dumps(data)}\n\n" -# ) - -# await send({ -# "type": "http.response.body", -# "body": event_str.encode("utf-8"), -# "more_body": True -# }) - -# last_id = notification.id - -# await asyncio.sleep(2) - -# except asyncio.CancelledError: -# pass - import json import time from django.contrib.auth.models import AnonymousUser diff --git a/inventory/override.py b/inventory/override.py index f95ca4b3..d89f95a9 100644 --- a/inventory/override.py +++ b/inventory/override.py @@ -411,6 +411,22 @@ class BasePurchaseOrderActionActionView( f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " f"Error: {e}" ) + except Exception as e: + print( + f"User {user_username} encountered an exception " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + logger.warning( + f"User {user_username} encountered an exception " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + messages.add_message( + request, + message=f"Failed to update PO {po_model.po_number}. {e}", + level=messages.ERROR, + ) return response @@ -1129,6 +1145,7 @@ class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, Creat }, ) + class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView): context_object_name = "coa_model" slug_url_kwarg = "coa_slug" diff --git a/inventory/signals.py b/inventory/signals.py index 7d38be84..649af178 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -5,6 +5,7 @@ from django.urls import reverse from django.contrib.auth.models import Group from django.db.models.signals import post_save, post_delete from django.dispatch import receiver + # from appointment.models import Service from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType @@ -21,7 +22,7 @@ from django_ledger.models import ( EstimateModel, BillModel, ChartOfAccountModel, - CustomerModel + CustomerModel, ) from . import models from django.utils.timezone import now @@ -136,7 +137,6 @@ def create_car_location(sender, instance, created, **kwargs): print(f"Failed to create CarLocation for car {instance.vin}: {e}") - @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): if not created: @@ -155,20 +155,22 @@ def create_ledger_entity(sender, instance, created, **kwargs): raise Exception("Entity creation failed") instance.entity = entity - instance.save(update_fields=['entity']) + instance.save(update_fields=["entity"]) # Create default COA entity.create_chart_of_accounts( - assign_as_default=True, - commit=True, - coa_name=f"{entity.name}-COA" + assign_as_default=True, commit=True, coa_name=f"{entity.name}-COA" ) - logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.") + logger.info( + f"✅ Setup complete for dealer {instance.id}: entity & COA ready." + ) except Exception as e: logger.error(f"💥 Failed setup for dealer {instance.id}: {e}") # Optional: schedule retry or alert + + # Create Entity # @receiver(post_save, sender=models.Dealer) # def create_ledger_entity(sender, instance, created, **kwargs): @@ -218,10 +220,10 @@ def create_ledger_entity(sender, instance, created, **kwargs): # dealer=instance, # hook="inventory.hooks.check_create_coa_accounts", # ) - # async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20)) +# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20)) - # create_settings(instance.pk) - # create_accounts_for_make(instance.pk) +# create_settings(instance.pk) +# create_accounts_for_make(instance.pk) @receiver(post_save, sender=models.Dealer) @@ -998,16 +1000,28 @@ def save_po(sender, instance, created, **kwargs): instance.itemtransactionmodel_set.first().po_model.save() except Exception as e: pass + + @receiver(post_save, sender=PurchaseOrderModel) def create_po_item_upload(sender, instance, created, **kwargs): - if instance.po_status == "fulfilled" or instance.po_status == 'approved': + if instance.po_status == "fulfilled" or instance.po_status == "approved": for item in instance.get_itemtxs_data()[0]: dealer = models.Dealer.objects.get(entity=instance.entity) - if item.bill_model.is_paid(): - models.PoItemsUploaded.objects.get_or_create( - dealer=dealer, po=instance, item=item, status=instance.po_status + if item.bill_model and item.bill_model.is_paid(): + models.PoItemsUploaded.objects.update_or_create( + dealer=dealer, + po=instance, + item=item, + defaults={"status": instance.po_status}, ) + # po_item = models.PoItemsUploaded.objects.get_or_create( + # dealer=dealer, po=instance, item=item, + # defaults={ + # "status":instance.po_status + # } + # ) + # @receiver(post_save, sender=models.Staff) # def add_service_to_staff(sender, instance, created, **kwargs): @@ -1354,7 +1368,9 @@ def handle_car_image(sender, instance, created, **kwargs): # ) # Check for existing image with same hash - existing = os.path.exists(os.path.join(settings.MEDIA_ROOT, "car_images",car.get_hash + ".png")) + existing = os.path.exists( + os.path.join(settings.MEDIA_ROOT, "car_images", car.get_hash + ".png") + ) # existing = ( # models.CarImage.objects.filter( # image_hash=car.get_hash, image__isnull=False @@ -1396,7 +1412,7 @@ def handle_user_registration(sender, instance, created, **kwargs): """ Thank you for registering with us. We will contact you shortly to complete your application. شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك. - """ + """, ) if instance.is_created: @@ -1420,7 +1436,8 @@ def handle_user_registration(sender, instance, created, **kwargs): يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا. شكرا لاختيارك لنا. - """) + """, + ) @receiver(post_save, sender=ChartOfAccountModel) @@ -1453,4 +1470,4 @@ def handle_chart_of_account(sender, instance, created, **kwargs): # sync=False # Explicitly set to async # ) except Exception as e: - logger.error(f"Error handling chart of account: {e}") \ No newline at end of file + logger.error(f"Error handling chart of account: {e}") diff --git a/inventory/tasks.py b/inventory/tasks.py index a0a3875a..d40683a3 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -12,12 +12,14 @@ from django.db import transaction from django_ledger.io import roles from django_q.tasks import async_task from django.core.mail import send_mail + # from appointment.models import StaffMember from django.utils.translation import activate from django.core.files.base import ContentFile from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress from django.core.mail import EmailMultiAlternatives + # from .utils import get_accounts_data, create_account from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ @@ -30,7 +32,7 @@ from inventory.models import ( CarReservation, CarStatusChoices, CarImage, - Car + Car, ) logger = logging.getLogger(__name__) @@ -63,17 +65,18 @@ def create_settings(pk): ) -def create_coa_accounts(dealer_id,**kwargs): +def create_coa_accounts(dealer_id, **kwargs): """ Idempotent: Creates only missing default accounts. Safe to retry. Returns True if all done. """ from .models import Dealer from .utils import get_accounts_data, create_account + try: dealer = Dealer.objects.get(pk=dealer_id) entity = dealer.entity - coa_slug = kwargs.get('coa_slug', None) + coa_slug = kwargs.get("coa_slug", None) if not entity: logger.error(f"❌ No entity for dealer {dealer_id}") return False @@ -82,7 +85,9 @@ def create_coa_accounts(dealer_id,**kwargs): try: coa = entity.get_coa_model_qs().get(slug=coa_slug) except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") + logger.error( + f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}" + ) return False else: coa = entity.get_default_coa() @@ -92,10 +97,13 @@ def create_coa_accounts(dealer_id,**kwargs): return False # Get missing accounts - existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True)) + existing_codes = set( + entity.get_all_accounts() + .filter(coa_model=coa) + .values_list("code", flat=True) + ) accounts_to_create = [ - acc for acc in get_accounts_data() - if acc["code"] not in existing_codes + acc for acc in get_accounts_data() if acc["code"] not in existing_codes ] if not accounts_to_create: @@ -122,6 +130,7 @@ def create_coa_accounts(dealer_id,**kwargs): logger.error(f"💥 Task failed for dealer {dealer_id}: {e}") raise # Let Django-Q handle retry if configured + def retry_entity_creation(dealer_id, retry_count=0): """ Retry entity creation if initial attempt failed @@ -164,8 +173,10 @@ def retry_entity_creation(dealer_id, retry_count=0): async_task( "inventory.tasks.retry_entity_creation", dealer_id=dealer_id, - retry_count=retry_count + 1 + retry_count=retry_count + 1, ) + + # def create_coa_accounts(**kwargs): # logger.info("creating all accounts are created") # instance = kwargs.get("dealer") diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index fb73e6d3..e013f164 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -13,6 +13,7 @@ from django.db.models import Case, Value, When, IntegerField register = template.Library() + @register.filter def is_negative(value): """ @@ -23,6 +24,7 @@ def is_negative(value): except (ValueError, TypeError): return False + @register.filter def get_percentage(value, total): try: @@ -501,8 +503,16 @@ def bill_item_formset_table(context, item_formset): for item in item_formset: if item: print(item.fields["item_model"]) - item.initial["quantity"] = item.instance.po_quantity if item.instance.po_quantity else item.instance.quantity - item.initial["unit_cost"] = item.instance.po_unit_cost if item.instance.po_unit_cost else item.instance.unit_cost + item.initial["quantity"] = ( + item.instance.po_quantity + if item.instance.po_quantity + else item.instance.quantity + ) + item.initial["unit_cost"] = ( + item.instance.po_unit_cost + if item.instance.po_unit_cost + else item.instance.unit_cost + ) # print(item.instance.po_quantity) # print(item.instance.po_unit_cost) # print(item.instance.po_total_amount) diff --git a/inventory/urls.py b/inventory/urls.py index 8a994195..9236ab3b 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -2,7 +2,7 @@ from inventory.utils import get_user_type from . import views from django.urls import path from django.urls import reverse_lazy -from django.views.generic import RedirectView,TemplateView +from django.views.generic import RedirectView, TemplateView from django_tables2.export.export import TableExport from django.conf.urls import handler403, handler400, handler404, handler500 @@ -10,14 +10,17 @@ urlpatterns = [ # main URLs path("", views.WelcomeView, name="welcome"), # path("signup/", views.dealer_signup, name="account_signup"), - path('signup/', views.CarDealershipSignUpView.as_view(), name='account_signup'), - path('success/', TemplateView.as_view(template_name='account/success.html'), name='registration_success'), + path("signup/", views.CarDealershipSignUpView.as_view(), name="account_signup"), + path( + "success/", + TemplateView.as_view(template_name="account/success.html"), + name="registration_success", + ), path("", views.HomeView, name="home"), # path('refund-policy/',views.refund_policy,name='refund_policy'), path("/", views.HomeView, name="home"), # Tasks path("legal/", views.terms_and_privacy, name="terms_and_privacy"), - # path('tasks//detail/', views.task_detail, name='task_detail'), # Dashboards # path("user//settings/", views.UserSettingsView.as_view(), name="user_settings"), @@ -44,13 +47,18 @@ urlpatterns = [ views.assign_car_makes, name="assign_car_makes", ), - - - #dashboards for manager, dealer, inventory and accounatant - path("dashboards//general/", views.general_dashboard,name="general_dashboard"), - #dashboard for sales - path("dashboards//sales/", views.sales_dashboard, name="sales_dashboard"), - + # dashboards for manager, dealer, inventory and accounatant + path( + "dashboards//general/", + views.general_dashboard, + name="general_dashboard", + ), + # dashboard for sales + path( + "dashboards//sales/", + views.sales_dashboard, + name="sales_dashboard", + ), path( "/cars/aging-inventory/list", views.aging_inventory_list_view, @@ -786,7 +794,11 @@ urlpatterns = [ views.EstimateDetailView.as_view(), name="estimate_detail", ), - path('/sales/estimates/print//', views.EstimatePrintView.as_view(), name='estimate_print'), + path( + "/sales/estimates/print//", + views.EstimatePrintView.as_view(), + name="estimate_print", + ), path( "/sales/estimates/create/", views.create_estimate, @@ -794,7 +806,7 @@ urlpatterns = [ ), path( "/sales/estimates/create//", - views.create_estimate, + views.estimate_create_from_opportunity, name="estimate_create_from_opportunity", ), path( @@ -943,7 +955,6 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), - path( "/items/services//detail/", views.ItemServiceDetailView.as_view(), @@ -1112,32 +1123,47 @@ urlpatterns = [ name="entity-ic-date", ), # Chart of Accounts... - path('/chart-of-accounts//list/', + path( + "/chart-of-accounts//list/", views.ChartOfAccountModelListView.as_view(), - name='coa-list'), - path('/chart-of-accounts//list/inactive/', - views.ChartOfAccountModelListView.as_view(inactive=True), - name='coa-list-inactive'), - path('//create/', - views.ChartOfAccountModelCreateView.as_view(), - name='coa-create'), - path('//detail//', - views.ChartOfAccountModelListView.as_view(), - name='coa-detail'), - path('//update//', - views.ChartOfAccountModelUpdateView.as_view(), - name='coa-update'), - + name="coa-list", + ), + path( + "/chart-of-accounts//list/inactive/", + views.ChartOfAccountModelListView.as_view(inactive=True), + name="coa-list-inactive", + ), + path( + "//create/", + views.ChartOfAccountModelCreateView.as_view(), + name="coa-create", + ), + path( + "//detail//", + views.ChartOfAccountModelListView.as_view(), + name="coa-detail", + ), + path( + "//update//", + views.ChartOfAccountModelUpdateView.as_view(), + name="coa-update", + ), # ACTIONS.... - path('//action//mark-as-default/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'), - name='coa-action-mark-as-default'), - path('//action//mark-as-active/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'), - name='coa-action-mark-as-active'), - path('//action//mark-as-inactive/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'), - name='coa-action-mark-as-inactive'), + path( + "//action//mark-as-default/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"), + name="coa-action-mark-as-default", + ), + path( + "//action//mark-as-active/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"), + name="coa-action-mark-as-active", + ), + path( + "//action//mark-as-inactive/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"), + name="coa-action-mark-as-inactive", + ), # CASH FLOW STATEMENTS... # Entities... path( @@ -1313,42 +1339,80 @@ urlpatterns = [ views.PurchaseOrderMarkAsVoidView.as_view(), name="po-action-mark-as-void", ), - # reports - path( + path( "/purchase-report/", views.purchase_report_view, name="po-report", ), - path('purchase-report//csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'), - - path( + path( + "purchase-report//csv/", + views.purchase_report_csv_export, + name="purchase-report-csv-export", + ), + path( "/car-sale-report/", views.car_sale_report_view, name="car-sale-report", ), - path('/car-sale-report/get_filtered_choices/',views.get_filtered_choices,name='get_filtered_choices'), - path('car-sale-report//csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'), - - path('feature/recall/', views.RecallListView.as_view(), name='recall_list'), - path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'), - path('feature/recall//view/', views.RecallDetailView.as_view(), name='recall_detail'), - path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'), - path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'), - - path('/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), - + path( + "/car-sale-report/get_filtered_choices/", + views.get_filtered_choices, + name="get_filtered_choices", + ), + path( + "car-sale-report//csv/", + views.car_sale_report_csv_export, + name="car-sale-report-csv-export", + ), + path("feature/recall/", views.RecallListView.as_view(), name="recall_list"), + path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"), + path( + "feature/recall//view/", + views.RecallDetailView.as_view(), + name="recall_detail", + ), + path( + "feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create" + ), + path( + "feature/recall/success/", + views.RecallSuccessView.as_view(), + name="recall_success", + ), + path( + "/schedules/calendar/", + views.schedule_calendar, + name="schedule_calendar", + ), # staff profile - path('/staff/detail/', views.StaffDetailView.as_view(), name='staff_detail'), + path( + "/staff/detail/", + views.StaffDetailView.as_view(), + name="staff_detail", + ), # tickets - path('help_center/view/', views.help_center, name='help_center'), - path('/help_center/tickets/', views.ticket_list, name='ticket_list'), - path('help_center/tickets//create/', views.create_ticket, name='create_ticket'), - path('/help_center/tickets//', views.ticket_detail, name='ticket_detail'), - path('help_center/tickets//update/', views.ticket_update, name='ticket_update'), + path("help_center/view/", views.help_center, name="help_center"), + path( + "/help_center/tickets/", views.ticket_list, name="ticket_list" + ), + path( + "help_center/tickets//create/", + views.create_ticket, + name="create_ticket", + ), + path( + "/help_center/tickets//", + views.ticket_detail, + name="ticket_detail", + ), + path( + "help_center/tickets//update/", + views.ticket_update, + name="ticket_update", + ), # path('help_center/tickets//ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), - path('payment_results/', views.payment_result, name='payment_result'), - + path("payment_results/", views.payment_result, name="payment_result"), ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/utils.py b/inventory/utils.py index 4c8caf36..350b18ac 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -27,7 +27,7 @@ from django_ledger.models import ( VendorModel, AccountModel, EntityModel, - ChartOfAccountModel + ChartOfAccountModel, ) from django.core.files.base import ContentFile from django_ledger.models.items import ItemModel @@ -1325,11 +1325,13 @@ def get_finance_data(estimate, dealer): ) discount = extra_info.data.get("discount", 0) discount = Decimal(discount) - additional_services = car.get_additional_services() + discounted_price = Decimal(car.marked_price) - discount vat_amount = discounted_price * vat.rate + total_services_amount = additional_services.get("total") total_services_vat = sum([x[1] for x in additional_services.get("services")]) + total_services_amount_ = additional_services.get("total_") total_vat = vat_amount + total_services_vat return { "car": car, @@ -1341,6 +1343,8 @@ def get_finance_data(estimate, dealer): "additional_services": additional_services, "final_price": discounted_price + vat_amount, "total_services_vat": total_services_vat, + "total_services_amount": total_services_amount, + "total_services_amount_": total_services_amount_, "total_vat": total_vat, "grand_total": discounted_price + total_vat + additional_services.get("total"), } @@ -1596,43 +1600,69 @@ def _post_sale_and_cogs(invoice, dealer): 1) Cash / A-R / VAT / Revenue journal 2) COGS / Inventory journal """ - entity:EntityModel = invoice.ledger.entity + entity: EntityModel = invoice.ledger.entity # calc = CarFinanceCalculator(invoice) data = get_finance_data(invoice, dealer) car = data.get("car") - coa:ChartOfAccountModel = entity.get_default_coa() + coa: ChartOfAccountModel = entity.get_default_coa() cash_acc = invoice.cash_account or dealer.settings.invoice_cash_account - vat_acc = dealer.settings.invoice_tax_payable_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() + vat_acc = ( + dealer.settings.invoice_tax_payable_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE) + .first() + ) - car_rev = dealer.settings.invoice_vehicle_sale_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() + car_rev = ( + dealer.settings.invoice_vehicle_sale_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.INCOME_OPERATIONAL) + .first() + ) add_rev = dealer.settings.invoice_additional_services_account if not add_rev: try: - add_rev = entity.get_default_coa_accounts().filter(name="After-Sales Services", active=True).first() + add_rev = ( + entity.get_default_coa_accounts() + .filter(name="After-Sales Services", active=True) + .first() + ) if not add_rev: add_rev = coa.create_account( - code="4020", - name="After-Sales Services", - role=roles.INCOME_OPERATIONAL, - balance_type=roles.CREDIT, - active=True, + code="4020", + name="After-Sales Services", + role=roles.INCOME_OPERATIONAL, + balance_type=roles.CREDIT, + active=True, ) add_rev.role_default = False - add_rev.save(update_fields=['role_default']) + add_rev.save(update_fields=["role_default"]) dealer.settings.invoice_additional_services_account = add_rev dealer.settings.save() except Exception as e: logger.error(f"error find or create additional services account {e}") if car.get_additional_services_amount > 0 and not add_rev: - raise Exception("additional services exist but not account found,please create account for the additional services and set as default in the settings") - cogs_acc = dealer.settings.invoice_cost_of_good_sold_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() + raise Exception( + "additional services exist but not account found,please create account for the additional services and set as default in the settings" + ) + cogs_acc = ( + dealer.settings.invoice_cost_of_good_sold_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.COGS) + .first() + ) - inv_acc = dealer.settings.invoice_inventory_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() + inv_acc = ( + dealer.settings.invoice_inventory_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.ASSET_CA_INVENTORY) + .first() + ) net_car_price = Decimal(data["discounted_price"]) net_additionals_price = Decimal(data["additional_services"]["total"]) @@ -1687,11 +1717,12 @@ def _post_sale_and_cogs(invoice, dealer): # tx_type='credit' # ) - if car.get_additional_services_amount > 0: # Cr Sales – Additional Services if not add_rev: - logger.warning(f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry.") + logger.warning( + f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry." + ) else: TransactionModel.objects.create( journal_entry=je_sale, @@ -1929,7 +1960,7 @@ def handle_payment(request, dealer): } # Get selected plan from session - selected_plan_id = request.session.get('pending_plan_id') + selected_plan_id = request.session.get("pending_plan_id") if not selected_plan_id: raise ValueError("No pending plan found in session") from plans.models import PlanPricing @@ -1947,25 +1978,27 @@ def handle_payment(request, dealer): "dealer_slug": dealer.slug, } - payload = json.dumps({ - "amount": total, - "currency": "SAR", - "description": f"Payment for plan {pp.plan.name}", - "callback_url": callback_url, - "source": { - "type": "creditcard", - "name": card_name, - "number": card_number, - "month": month, - "year": year, - "cvc": cvv, - "statement_descriptor": "Century Store", - "3ds": True, - "manual": False, - "save_card": False, - }, - "metadata": metadata, - }) + payload = json.dumps( + { + "amount": total, + "currency": "SAR", + "description": f"Payment for plan {pp.plan.name}", + "callback_url": callback_url, + "source": { + "type": "creditcard", + "name": card_name, + "number": card_number, + "month": month, + "year": year, + "cvc": cvv, + "statement_descriptor": "Century Store", + "3ds": True, + "manual": False, + "save_card": False, + }, + "metadata": metadata, + } + ) headers = {"Content-Type": "application/json", "Accept": "application/json"} auth = (settings.MOYASAR_SECRET_KEY, "") @@ -1989,7 +2022,9 @@ def handle_payment(request, dealer): gateway_response=data, ) logger.info(f"Payment initiated: {data}") - return data["source"]["transaction_url"],None + return data["source"]["transaction_url"], None + + # def handle_payment(request, order): # logger.info(f"Handling payment for order {order}") # url = "https://api.moyasar.com/v1/payments" @@ -2509,10 +2544,9 @@ def create_account(entity, coa, account_data): logger.info(f"Created account: {account}") if account: account.role_default = account_data.get("default", False) - account.save(update_fields=['role_default']) + account.save(update_fields=["role_default"]) return True - except IntegrityError: return True # Already created by race condition except Exception as e: @@ -2520,6 +2554,7 @@ def create_account(entity, coa, account_data): return False + # def create_account(entity, coa, account_data): # try: # account = entity.create_account( @@ -2801,7 +2836,9 @@ def generate_car_image_simple(car): # Save the resized image logger.info(f" {car.vin}") with open( - os.path.join(settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"), + os.path.join( + settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}" + ), "wb", ) as f: f.write(resized_data) @@ -2817,9 +2854,7 @@ def generate_car_image_simple(car): return {"success": False, "error": error_msg} - - -def create_estimate_(dealer,car,customer): +def create_estimate_(dealer, car, customer): entity = dealer.entity title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}" estimate = entity.create_estimate( @@ -2847,4 +2882,4 @@ def create_estimate_(dealer,car,customer): estimate.delete() raise e - return estimate \ No newline at end of file + return estimate diff --git a/inventory/validators.py b/inventory/validators.py index 791df1dc..856778ef 100644 --- a/inventory/validators.py +++ b/inventory/validators.py @@ -16,9 +16,10 @@ class SaudiPhoneNumberValidator(RegexValidator): cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value)) super().__call__(cleaned_value) + def vat_rate_validator(value): if value < 0 or value > 1: raise ValidationError( - _('%(value)s is not a valid VAT rate. It must be between 0 and 1.'), - params={'value': value}, - ) \ No newline at end of file + _("%(value)s is not a valid VAT rate. It must be between 0 and 1."), + params={"value": value}, + ) diff --git a/inventory/views.py b/inventory/views.py index fe6584f2..5d8256c8 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -19,7 +19,7 @@ from random import randint from decimal import Decimal from io import TextIOWrapper from django.apps import apps -from datetime import datetime, timedelta,date +from datetime import datetime, timedelta, date from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -37,6 +37,7 @@ from django.core.exceptions import PermissionDenied from django.contrib.contenttypes.models import ContentType from django.views.decorators.http import require_POST from django.template.loader import render_to_string + # Django from django.db.models import Q from django.conf import settings @@ -54,7 +55,7 @@ from django.forms import CharField, HiddenInput, ValidationError from django.shortcuts import HttpResponse from django.db.models import Sum, F, Count -from django.db.models.functions import ExtractMonth,Round +from django.db.models.functions import ExtractMonth, Round from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.auth.models import User, Group from django.db.models import Value @@ -109,7 +110,7 @@ from django_ledger.forms.bank_account import ( BankAccountUpdateForm, ) from django_ledger.views.chart_of_accounts import ( - ChartOfAccountModelListView as ChartOfAccountModelListViewBase + ChartOfAccountModelListView as ChartOfAccountModelListViewBase, ) from django_ledger.views.bill import ( BillModelCreateView, @@ -175,7 +176,7 @@ from django_ledger.models import ( BillModel, LedgerModel, PurchaseOrderModel, - ChartOfAccountModel + ChartOfAccountModel, ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -198,6 +199,7 @@ from django_ledger.views.mixins import ( from . import models, forms, tables from django_tables2 import SingleTableView from django_tables2.export.views import ExportMixin + # from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from .services import ( decodevin, @@ -291,8 +293,10 @@ def switch_language(request): logger.warning(f"Invalid language code: {language}") return redirect("/") + def dealer_signup(request): from django_q.tasks import async_task + """ Handles the dealer signup wizard process, including forms validation, user and group creation, permissions assignment, and dealer data storage. This view supports GET @@ -365,6 +369,7 @@ def dealer_signup(request): "account/signup-wizar.html", ) + # class HomeView(LoginRequiredMixin, TemplateView): # """ # HomeView class responsible for rendering the home page. @@ -383,13 +388,15 @@ def dealer_signup(request): # template_name = "index.html" + @login_required -def HomeView(request,dealer_slug=None): - dealer_slug=request.dealer.slug +def HomeView(request, dealer_slug=None): + dealer_slug = request.dealer.slug if request.is_sales and not request.is_manager and not request.is_dealer: - return redirect('sales_dashboard', dealer_slug=dealer_slug) + return redirect("sales_dashboard", dealer_slug=dealer_slug) else: - return redirect('general_dashboard',dealer_slug=dealer_slug) + return redirect("general_dashboard", dealer_slug=dealer_slug) + class TestView(TemplateView): """ @@ -407,25 +414,26 @@ class TestView(TemplateView): template_name = "inventory/cars_list_api.html" + @login_required -def general_dashboard(request,dealer_slug): +def general_dashboard(request, dealer_slug): """ Renders the dealer dashboard with key performance indicators and chart data. """ - dealer = get_object_or_404(models.Dealer,slug=dealer_slug) - vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() - VAT_RATE=vat.rate + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() + VAT_RATE = vat.rate today_local = timezone.localdate() # ---------------------------------------------------- # 1. Date Filtering # ---------------------------------------------------- - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str and end_date_str: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date() else: start_date = today_local - timedelta(days=30) end_date = today_local @@ -433,104 +441,152 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- # 2. Inventory KPIs # ---------------------------------------------------- - active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold") total_cars_in_inventory = active_cars.count() - total_inventory_value = active_cars.aggregate(total=Sum('cost_price'))['total'] or 0 - new_cars_qs = active_cars.filter(stock_type='new') + total_inventory_value = active_cars.aggregate(total=Sum("cost_price"))["total"] or 0 + new_cars_qs = active_cars.filter(stock_type="new") total_new_cars_in_inventory = new_cars_qs.count() - new_car_value = new_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 - used_cars_qs = active_cars.filter(stock_type='used') + new_car_value = new_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0 + used_cars_qs = active_cars.filter(stock_type="used") total_used_cars_in_inventory = used_cars_qs.count() - used_car_value = used_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 + used_car_value = used_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0 aging_threshold_days = 60 - aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() + aging_inventory_count = active_cars.filter( + receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days) + ).count() # ---------------------------------------------------- # 3. Sales KPIs (filtered by date) # ---------------------------------------------------- cars_sold_filtered = models.Car.objects.filter( dealer=dealer, - status='sold', + status="sold", sold_date__date__gte=start_date, - sold_date__date__lte=end_date + sold_date__date__lte=end_date, ) # General sales KPIs total_cars_sold = cars_sold_filtered.count() - total_cost_of_cars_sold = cars_sold_filtered.aggregate(total=Sum('cost_price'))['total'] or 0 - total_revenue_from_cars = cars_sold_filtered.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_cost_of_cars_sold = ( + cars_sold_filtered.aggregate(total=Sum("cost_price"))["total"] or 0 + ) + total_revenue_from_cars = ( + cars_sold_filtered.aggregate( + total=Sum(F("marked_price") - F("discount_amount")) + )["total"] + or 0 + ) - total_vat_collected_from_cars = cars_sold_filtered.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_cars = ( + cars_sold_filtered.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) net_profit_from_cars = total_revenue_from_cars - total_cost_of_cars_sold - total_discount = cars_sold_filtered.aggregate(total=Sum('discount_amount'))['total'] or 0 + total_discount = ( + cars_sold_filtered.aggregate(total=Sum("discount_amount"))["total"] or 0 + ) # Sales breakdown by type - new_cars_sold = cars_sold_filtered.filter(stock_type='new') + new_cars_sold = cars_sold_filtered.filter(stock_type="new") total_new_cars_sold = new_cars_sold.count() - total_cost_of_new_cars_sold = new_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 + total_cost_of_new_cars_sold = ( + new_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0 + ) # total_revenue_from_new_cars=sum([ car.final_price for car in new_cars_sold]) - total_revenue_from_new_cars = new_cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_revenue_from_new_cars = ( + new_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_collected_from_new_cars = new_cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_new_cars = ( + new_cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) net_profit_from_new_cars = total_revenue_from_new_cars - total_cost_of_new_cars_sold - - - used_cars_sold = cars_sold_filtered.filter(stock_type='used') + used_cars_sold = cars_sold_filtered.filter(stock_type="used") total_used_cars_sold = used_cars_sold.count() - total_cost_of_used_cars_sold = used_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 - total_revenue_from_used_cars = used_cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_cost_of_used_cars_sold = ( + used_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0 + ) + total_revenue_from_used_cars = ( + used_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_collected_from_used_cars = used_cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_used_cars = ( + used_cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) - net_profit_from_used_cars = total_revenue_from_used_cars - total_cost_of_used_cars_sold + net_profit_from_used_cars = ( + total_revenue_from_used_cars - total_cost_of_used_cars_sold + ) # Service & Overall KPIs - total_revenue_from_services = sum([car.get_additional_services()['total'] for car in cars_sold_filtered]) - total_vat_collected_from_services = sum([car.get_additional_services()['services_vat'] for car in cars_sold_filtered]) - total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services + total_revenue_from_services = sum( + [car.get_additional_services()["total"] for car in cars_sold_filtered] + ) + total_vat_collected_from_services = sum( + [car.get_additional_services()["services_vat"] for car in cars_sold_filtered] + ) + total_vat_collected = ( + total_vat_collected_from_cars + total_vat_collected_from_services + ) total_revenue_generated = total_revenue_from_cars + total_revenue_from_services # total_expenses=sum([x.amount_paid for x in dealer.entity.get_bills().filter(bill_items__item_role="expense")]) - total_expenses=dealer.entity.get_bills().filter(bill_items__item_role="expense").aggregate(total=Sum('amount_paid'))['total'] or 0 - gross_profit = net_profit_from_cars+total_revenue_from_services - total_expenses + total_expenses = ( + dealer.entity.get_bills() + .filter(bill_items__item_role="expense") + .aggregate(total=Sum("amount_paid"))["total"] + or 0 + ) + gross_profit = net_profit_from_cars + total_revenue_from_services - total_expenses # ---------------------------------------------------- # 4. Chart Data Aggregation # ---------------------------------------------------- - monthly_sales_data = cars_sold_filtered.annotate( - month=ExtractMonth('sold_date') - ).values('month').annotate( - total_cars=Count('pk'), - total_revenue=Sum(F('marked_price') - F('discount_amount')), - total_profit=Sum(F('marked_price') - F('discount_amount') - F('cost_price')) - ).order_by('month') + monthly_sales_data = ( + cars_sold_filtered.annotate(month=ExtractMonth("sold_date")) + .values("month") + .annotate( + total_cars=Count("pk"), + total_revenue=Sum(F("marked_price") - F("discount_amount")), + total_profit=Sum( + F("marked_price") - F("discount_amount") - F("cost_price") + ), + ) + .order_by("month") + ) monthly_cars_sold = [0] * 12 monthly_revenue = [0] * 12 monthly_net_profit = [0] * 12 for data in monthly_sales_data: - month_index = data['month'] - 1 - monthly_cars_sold[month_index] = data['total_cars'] - monthly_revenue[month_index] = float(data['total_revenue']) if data['total_revenue'] else 0 - monthly_net_profit[month_index] = float(data['total_profit']) if data['total_profit'] else 0 + month_index = data["month"] - 1 + monthly_cars_sold[month_index] = data["total_cars"] + monthly_revenue[month_index] = ( + float(data["total_revenue"]) if data["total_revenue"] else 0 + ) + monthly_net_profit[month_index] = ( + float(data["total_profit"]) if data["total_profit"] else 0 + ) monthly_cars_sold_json = json.dumps(monthly_cars_sold) monthly_revenue_json = json.dumps(monthly_revenue) @@ -539,224 +595,217 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- # Sales by MAKE # ---------------------------------------------------- - sales_by_make_data = cars_sold_filtered.values('id_car_make__name').annotate( - car_count=Count('id_car_make__name') - ).order_by('-car_count') - - sales_by_make_labels = [data['id_car_make__name'] for data in sales_by_make_data] - sales_by_make_counts = [data['car_count'] for data in sales_by_make_data] - + sales_by_make_data = ( + cars_sold_filtered.values("id_car_make__name") + .annotate(car_count=Count("id_car_make__name")) + .order_by("-car_count") + ) + sales_by_make_labels = [data["id_car_make__name"] for data in sales_by_make_data] + sales_by_make_counts = [data["car_count"] for data in sales_by_make_data] # ---------------------------------------------------- # DATA FOR CAR SALES BY MODELS (for the new interactive chart) # ---------------------------------------------------- - # Get the selected make from the URL query parameter - selected_make_sales= request.GET.get('make_sold', None) - + selected_make_sales = request.GET.get("make_sold", None) # Get a list of all unique makes for the dropdown - all_makes_sold = list(cars_sold_filtered.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_sold = list( + cars_sold_filtered.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) if selected_make_sales: # If a make is selected, filter the queryset - sales_data_by_model = cars_sold_filtered.filter( - id_car_make__name=selected_make_sales - ).values('id_car_model__name').annotate( - count=Count('id_car_model__name') - ).order_by('-count') + sales_data_by_model = ( + cars_sold_filtered.filter(id_car_make__name=selected_make_sales) + .values("id_car_model__name") + .annotate(count=Count("id_car_model__name")) + .order_by("-count") + ) else: # If no make is selected, pass an empty list or some default data sales_data_by_model = [] - - - # 1. Inventory by Make (Pie Chart) - inventory_by_make_data = active_cars.values('id_car_make__name').annotate( - car_count=Count('id_car_make__name') - ).order_by('-car_count') + inventory_by_make_data = ( + active_cars.values("id_car_make__name") + .annotate(car_count=Count("id_car_make__name")) + .order_by("-car_count") + ) - inventory_by_make_labels = [data['id_car_make__name'] for data in inventory_by_make_data] - inventory_by_make_counts = [data['car_count'] for data in inventory_by_make_data] + inventory_by_make_labels = [ + data["id_car_make__name"] for data in inventory_by_make_data + ] + inventory_by_make_counts = [data["car_count"] for data in inventory_by_make_data] # 2. Inventory by Model (Bar Chart) - selected_make_inventory = request.GET.get('make_inventory', None) + selected_make_inventory = request.GET.get("make_inventory", None) # Get all unique makes in inventory for the dropdown - all_makes_inventory = list(active_cars.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_inventory = list( + active_cars.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) if selected_make_inventory: - inventory_data_by_model = active_cars.filter( - id_car_make__name=selected_make_inventory - ).values('id_car_model__name').annotate( - count=Count('id_car_model__name') - ).order_by('-count') + inventory_data_by_model = ( + active_cars.filter(id_car_make__name=selected_make_inventory) + .values("id_car_model__name") + .annotate(count=Count("id_car_model__name")) + .order_by("-count") + ) else: # Default data inventory_data_by_model = [] context = { - 'start_date': start_date, - 'end_date': end_date, - 'today': today_local, - + "start_date": start_date, + "end_date": end_date, + "today": today_local, # Inventory KPIs - 'total_cars_in_inventory': total_cars_in_inventory, - 'total_inventory_value': total_inventory_value, - 'total_new_cars_in_inventory': total_new_cars_in_inventory, - 'total_used_cars_in_inventory': total_used_cars_in_inventory, - 'new_car_value': new_car_value, - 'used_car_value': used_car_value, - 'aging_inventory_count': aging_inventory_count, - + "total_cars_in_inventory": total_cars_in_inventory, + "total_inventory_value": total_inventory_value, + "total_new_cars_in_inventory": total_new_cars_in_inventory, + "total_used_cars_in_inventory": total_used_cars_in_inventory, + "new_car_value": new_car_value, + "used_car_value": used_car_value, + "aging_inventory_count": aging_inventory_count, # Sales KPIs - 'total_cars_sold': total_cars_sold, - 'total_cost_of_cars_sold': total_cost_of_cars_sold, - 'total_revenue_from_cars': total_revenue_from_cars, - 'net_profit_from_cars': net_profit_from_cars, - 'total_vat_collected_from_cars': total_vat_collected_from_cars, - 'total_discount_on_cars': total_discount, - + "total_cars_sold": total_cars_sold, + "total_cost_of_cars_sold": total_cost_of_cars_sold, + "total_revenue_from_cars": total_revenue_from_cars, + "net_profit_from_cars": net_profit_from_cars, + "total_vat_collected_from_cars": total_vat_collected_from_cars, + "total_discount_on_cars": total_discount, # Sales by Type - 'total_new_cars_sold': total_new_cars_sold, - 'total_used_cars_sold': total_used_cars_sold, - 'total_cost_of_new_cars_sold': total_cost_of_new_cars_sold, - 'total_revenue_from_new_cars': total_revenue_from_new_cars, - 'net_profit_from_new_cars': net_profit_from_new_cars, - 'total_vat_collected_from_new_cars': total_vat_collected_from_new_cars, - 'total_cost_of_used_cars_sold': total_cost_of_used_cars_sold, - 'total_revenue_from_used_cars': total_revenue_from_used_cars, - 'net_profit_from_used_cars': net_profit_from_used_cars, - 'total_vat_collected_from_used_cars': total_vat_collected_from_used_cars, - + "total_new_cars_sold": total_new_cars_sold, + "total_used_cars_sold": total_used_cars_sold, + "total_cost_of_new_cars_sold": total_cost_of_new_cars_sold, + "total_revenue_from_new_cars": total_revenue_from_new_cars, + "net_profit_from_new_cars": net_profit_from_new_cars, + "total_vat_collected_from_new_cars": total_vat_collected_from_new_cars, + "total_cost_of_used_cars_sold": total_cost_of_used_cars_sold, + "total_revenue_from_used_cars": total_revenue_from_used_cars, + "net_profit_from_used_cars": net_profit_from_used_cars, + "total_vat_collected_from_used_cars": total_vat_collected_from_used_cars, # Services and Overall KPIs - 'total_revenue_from_services': total_revenue_from_services, - 'total_vat_collected_from_services': total_vat_collected_from_services, - 'total_revenue_generated': total_revenue_generated, - 'total_vat_collected': total_vat_collected, - 'total_expenses': total_expenses, - 'gross_profit': gross_profit, - + "total_revenue_from_services": total_revenue_from_services, + "total_vat_collected_from_services": total_vat_collected_from_services, + "total_revenue_generated": total_revenue_generated, + "total_vat_collected": total_vat_collected, + "total_expenses": total_expenses, + "gross_profit": gross_profit, # Chart Data - - 'monthly_cars_sold_json': monthly_cars_sold_json, - 'monthly_revenue_json': monthly_revenue_json, - 'monthly_net_profit_json': monthly_net_profit_json, - - - # Sales Chart Data - 'sales_by_make_labels_json': json.dumps(sales_by_make_labels), - 'sales_by_make_counts_json': json.dumps(sales_by_make_counts), - 'all_makes_sold': all_makes_sold, - 'selected_make_sales': selected_make_sales, - 'sales_data_by_model_json': json.dumps(list(sales_data_by_model)), - + "monthly_cars_sold_json": monthly_cars_sold_json, + "monthly_revenue_json": monthly_revenue_json, + "monthly_net_profit_json": monthly_net_profit_json, + # Sales Chart Data + "sales_by_make_labels_json": json.dumps(sales_by_make_labels), + "sales_by_make_counts_json": json.dumps(sales_by_make_counts), + "all_makes_sold": all_makes_sold, + "selected_make_sales": selected_make_sales, + "sales_data_by_model_json": json.dumps(list(sales_data_by_model)), # New Inventory Chart Data - 'inventory_by_make_labels_json': json.dumps(inventory_by_make_labels), - 'inventory_by_make_counts_json': json.dumps(inventory_by_make_counts), - 'all_makes_inventory': all_makes_inventory, - 'selected_make_inventory': selected_make_inventory, - 'inventory_data_by_model_json': json.dumps(list(inventory_data_by_model)), - - + "inventory_by_make_labels_json": json.dumps(inventory_by_make_labels), + "inventory_by_make_counts_json": json.dumps(inventory_by_make_counts), + "all_makes_inventory": all_makes_inventory, + "selected_make_inventory": selected_make_inventory, + "inventory_data_by_model_json": json.dumps(list(inventory_data_by_model)), } - - return render(request, 'dashboards/general_dashboard.html', context) + return render(request, "dashboards/general_dashboard.html", context) @login_required -def sales_dashboard(request,dealer_slug): - dealer = get_object_or_404(models.Dealer,slug=dealer_slug) +def sales_dashboard(request, dealer_slug): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) today_local = timezone.localdate() # ---------------------------------------------------- # 1. Date Filtering # ---------------------------------------------------- - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str and end_date_str: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date() else: start_date = today_local - timedelta(days=30) end_date = today_local # Filter leads by date range and dealer leads_filtered = models.Lead.objects.filter( - dealer=dealer, - created__date__gte=start_date, - created__date__lte=end_date + dealer=dealer, created__date__gte=start_date, created__date__lte=end_date ) - total_leads=leads_filtered.count() - + total_leads = leads_filtered.count() # ---------------------------------------------------- # 2. Lead Sources Chart Logic # ---------------------------------------------------- # Group leads by source and count them # This generates a list of dictionaries like [{'source': 'Showroom', 'count': 45}, ...] - lead_sources_data = leads_filtered.values('source').annotate( - count=Count('source') - ).order_by('-count') + lead_sources_data = ( + leads_filtered.values("source") + .annotate(count=Count("source")) + .order_by("-count") + ) # Separate the labels and counts for the chart - lead_sources_labels = [item['source'] for item in lead_sources_data] - lead_sources_counts = [item['count'] for item in lead_sources_data] + lead_sources_labels = [item["source"] for item in lead_sources_data] + lead_sources_counts = [item["count"] for item in lead_sources_data] # ---------------------------------------------------- # 2. Lead Funnel Chart Logic # ---------------------------------------------------- opportunity_filtered = models.Opportunity.objects.filter( - dealer=dealer, - created__date__gte=start_date, - created__date__lte=end_date + dealer=dealer, created__date__gte=start_date, created__date__lte=end_date ) - opportunity_stage_data = opportunity_filtered.values('stage').annotate( - count=Count('stage') - ).order_by('-count') - # Separate the labels and counts for the chart - opportunity_stage_labels = [item['stage'] for item in opportunity_stage_data ] - opportunity_stage_counts = [item['count'] for item in opportunity_stage_data ] - + opportunity_stage_data = ( + opportunity_filtered.values("stage") + .annotate(count=Count("stage")) + .order_by("-count") + ) + # Separate the labels and counts for the chart + opportunity_stage_labels = [item["stage"] for item in opportunity_stage_data] + opportunity_stage_counts = [item["count"] for item in opportunity_stage_data] # 2. Inventory KPIs # ---------------------------------------------------- - active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold") total_cars_in_inventory = active_cars.count() - new_cars_qs = active_cars.filter(stock_type='new') + new_cars_qs = active_cars.filter(stock_type="new") total_new_cars_in_inventory = new_cars_qs.count() - used_cars_qs = active_cars.filter(stock_type='used') + used_cars_qs = active_cars.filter(stock_type="used") total_used_cars_in_inventory = used_cars_qs.count() aging_threshold_days = 60 - aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() - + aging_inventory_count = active_cars.filter( + receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days) + ).count() context = { - 'start_date': start_date, - 'end_date': end_date, - 'lead_sources_labels_json': json.dumps(lead_sources_labels), - 'lead_sources_counts_json': json.dumps(lead_sources_counts), - 'opportunity_stage_labels_json': json.dumps(opportunity_stage_labels), - 'opportunity_stage_counts_json': json.dumps(opportunity_stage_counts), - - # Inventory KPIs - 'total_cars_in_inventory': total_cars_in_inventory, - 'total_new_cars_in_inventory': total_new_cars_in_inventory, - 'total_used_cars_in_inventory': total_used_cars_in_inventory, - 'aging_inventory_count': aging_inventory_count, - 'total_leads':total_leads + "start_date": start_date, + "end_date": end_date, + "lead_sources_labels_json": json.dumps(lead_sources_labels), + "lead_sources_counts_json": json.dumps(lead_sources_counts), + "opportunity_stage_labels_json": json.dumps(opportunity_stage_labels), + "opportunity_stage_counts_json": json.dumps(opportunity_stage_counts), + # Inventory KPIs + "total_cars_in_inventory": total_cars_in_inventory, + "total_new_cars_in_inventory": total_new_cars_in_inventory, + "total_used_cars_in_inventory": total_used_cars_in_inventory, + "aging_inventory_count": aging_inventory_count, + "total_leads": total_leads, } - return render(request, 'dashboards/sales_dashboard.html', context) - - + return render(request, "dashboards/sales_dashboard.html", context) def aging_inventory_list_view(request, dealer_slug): @@ -768,51 +817,80 @@ def aging_inventory_list_view(request, dealer_slug): aging_threshold_days = 60 # Get filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_series = request.GET.get('series') # Changed 'serie' to 'series' for consistency - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_series = request.GET.get( + "series" + ) # Changed 'serie' to 'series' for consistency + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") # Start with the base queryset for all aging cars. aging_cars_queryset = models.Car.objects.filter( dealer=dealer, - receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) - ).exclude(status='sold') + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days), + ).exclude(status="sold") # Apply filters to the queryset if they exist. Chaining is fine here. if selected_make: - aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_make__name=selected_make + ) if selected_model: - aging_cars_queryset = aging_cars_queryset.filter(id_car_model__name=selected_model) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_model__name=selected_model + ) if selected_series: - aging_cars_queryset = aging_cars_queryset.filter(id_car_series__name=selected_series) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_series__name=selected_series + ) if selected_year: - aging_cars_queryset = aging_cars_queryset.filter(id_car_year__year=selected_year) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_year__year=selected_year + ) if selected_stock_type: aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type) - total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total'] + total_aging_inventory_value = aging_cars_queryset.aggregate( + total=Sum("cost_price") + )["total"] count_of_aging_cars = aging_cars_queryset.count() - # Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset. # This ensures all possible filter options are always available. aging_base_queryset = models.Car.objects.filter( dealer=dealer, - receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) - ).exclude(status='sold') + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days), + ).exclude(status="sold") - all_makes = aging_base_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') - all_models = aging_base_queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name') - all_series = aging_base_queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name') - all_stock_types = aging_base_queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type') - all_years = aging_base_queryset.values_list('year', flat=True).distinct().order_by('-year') + all_makes = ( + aging_base_queryset.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) + all_models = ( + aging_base_queryset.values_list("id_car_model__name", flat=True) + .distinct() + .order_by("id_car_model__name") + ) + all_series = ( + aging_base_queryset.values_list("id_car_serie__name", flat=True) + .distinct() + .order_by("id_car_serie__name") + ) + all_stock_types = ( + aging_base_queryset.values_list("stock_type", flat=True) + .distinct() + .order_by("stock_type") + ) + all_years = ( + aging_base_queryset.values_list("year", flat=True).distinct().order_by("-year") + ) # # Set up pagination paginator = Paginator(aging_cars_queryset, 10) - page_number = request.GET.get('page') + page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) # Iterate only on the cars for the current page to add the age attribute. @@ -822,23 +900,23 @@ def aging_inventory_list_view(request, dealer_slug): context = { "is_paginated": page_obj.has_other_pages, "cars": page_obj.object_list, - 'selected_make': selected_make, - 'selected_model': selected_model, - 'selected_series': selected_series, # Corrected variable name - 'selected_year': selected_year, - 'selected_stock_type': selected_stock_type, - 'all_makes': all_makes, - 'all_models': all_models, - 'all_series': all_series, - 'all_stock_types': all_stock_types, - 'all_years': all_years, - 'total_aging_inventory_value':total_aging_inventory_value, - 'page_obj':page_obj, - 'count_of_aging_cars':count_of_aging_cars - + "selected_make": selected_make, + "selected_model": selected_model, + "selected_series": selected_series, # Corrected variable name + "selected_year": selected_year, + "selected_stock_type": selected_stock_type, + "all_makes": all_makes, + "all_models": all_models, + "all_series": all_series, + "all_stock_types": all_stock_types, + "all_years": all_years, + "total_aging_inventory_value": total_aging_inventory_value, + "page_obj": page_obj, + "count_of_aging_cars": count_of_aging_cars, } - return render(request, 'dashboards/aging_inventory_list.html', context) + return render(request, "dashboards/aging_inventory_list.html", context) + def terms_and_privacy(request): return render(request, "terms_and_privacy.html") @@ -863,7 +941,9 @@ def WelcomeView(request): return render(request, "welcome.html", context) -class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class CarCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ Manages the creation of a new car entry in the inventory system. @@ -885,7 +965,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi form_class = forms.CarForm template_name = "inventory/car_form.html" permission_required = ["inventory.add_car"] - success_message=_("Car Added successfully to the inventory") + success_message = _("Car Added successfully to the inventory") def get_form(self, form_class=None): form = super().get_form(form_class) @@ -976,7 +1056,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): def decode_vin(self, request): dealer = request.dealer vin_no = request.GET.get("vin_no") - car_existed = models.Car.objects.filter(dealer=dealer,vin=vin_no).exists() + car_existed = models.Car.objects.filter(dealer=dealer, vin=vin_no).exists() if car_existed: return JsonResponse({"error": _("VIN number exists")}, status=400) @@ -1284,7 +1364,9 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context -class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class CarColorCreate( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ View for creating a new car color. @@ -1308,7 +1390,7 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageM form_class = forms.CarColorsForm template_name = "inventory/add_colors.html" permission_required = ["inventory.add_carcolors"] - success_message=_("Car colors details added successfully") + success_message = _("Car colors details added successfully") def form_valid(self, form): car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) @@ -1593,7 +1675,11 @@ def inventory_stats_view(request, dealer_slug): for make_data in inventory.values() ], } - return render(request, "inventory/inventory_stats.html", {"inventory": result,"empty_state_value":_("car")}) + return render( + request, + "inventory/inventory_stats.html", + {"inventory": result, "empty_state_value": _("car")}, + ) # @login_required @@ -1743,14 +1829,16 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) form = forms.CarDetailsEstimateCreate() - form.fields["customer"].queryset = form.fields["customer"].queryset.filter(dealer=self.request.dealer) + form.fields["customer"].queryset = form.fields["customer"].queryset.filter( + dealer=self.request.dealer + ) context["estimate_form"] = form context["active_estimates"] = self.object.get_active_estimates() return context -def CarFinanceUpdateView(request,dealer_slug,slug): +def CarFinanceUpdateView(request, dealer_slug, slug): car = get_object_or_404(models.Car, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) @@ -1764,7 +1852,12 @@ def CarFinanceUpdateView(request,dealer_slug,slug): else: form = forms.CarFinanceForm(instance=car) - return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form}) + return render( + request, + "inventory/car_finance_form.html", + {"car": car, "dealer": dealer, "form": form}, + ) + class CarUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView @@ -1795,7 +1888,10 @@ class CarUpdateView( permission_required = ["inventory.change_car"] def get_success_url(self): - return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug}) + return reverse( + "car_detail", + kwargs={"dealer_slug": self.request.dealer.slug, "slug": self.object.slug}, + ) def get_form(self, form_class=None): form = super().get_form(form_class) @@ -2354,6 +2450,8 @@ class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): from .forms import VatRateForm + + @login_required def dealer_vat_rate_update(request, slug): dealer = get_object_or_404(models.Dealer, slug=slug) @@ -2361,7 +2459,6 @@ def dealer_vat_rate_update(request, slug): vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer) if request.method == "POST": - form = VatRateForm(request.POST, instance=vat_rate_instance) if form.is_valid(): @@ -2369,11 +2466,10 @@ def dealer_vat_rate_update(request, slug): messages.success(request, _("VAT rate updated successfully")) return redirect("dealer_detail", slug=slug) else: - messages.error(request, _("Please enter valid vat rate between 0 and 1.")) redirect("dealer_detail", slug=slug) - return redirect("dealer_detail", slug=slug) + return redirect("dealer_detail", slug=slug) class DealerUpdateView( @@ -2409,7 +2505,8 @@ class DealerUpdateView( def get_success_url(self): return reverse("dealer_detail", kwargs={"slug": self.object.slug}) -class StaffDetailView(LoginRequiredMixin, DetailView): + +class StaffDetailView(LoginRequiredMixin, DetailView): """ Represents a detailed view for a Dealer model. @@ -2431,9 +2528,6 @@ class StaffDetailView(LoginRequiredMixin, DetailView): context_object_name = "staff" - - - class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ View for displaying a list of customers. @@ -2525,6 +2619,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # context["note_form"] = forms.NoteForm() # return context + class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ CustomerDetailView handles retrieving and presenting detailed information about @@ -2554,13 +2649,11 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView context = super().get_context_data(**kwargs) context["notes"] = models.Notes.objects.filter( - dealer=dealer, - content_type__model="customer", object_id=self.object.id + dealer=dealer, content_type__model="customer", object_id=self.object.id ) estimates = entity.get_estimates().filter(customer=self.object.customer_model) invoices = entity.get_invoices().filter(customer=self.object.customer_model) - context['leads']=self.object.customer_leads.all() - + context["leads"] = self.object.customer_leads.all() total = estimates.count() + invoices.count() @@ -2675,8 +2768,7 @@ class CustomerCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) if customer := models.Customer.objects.filter( - dealer=dealer, - email=form.instance.email + dealer=dealer, email=form.instance.email ).first(): if not customer.active: messages.error( @@ -2855,16 +2947,25 @@ def vendorDetailView(request, dealer_slug, slug): :rtype: HttpResponse """ dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - vendor = get_object_or_404(models.Vendor, slug=slug,dealer=dealer) - cars=vendor.cars.all() - total_cars_from_vendor=cars.count() - vendor_makes=cars.values('id_car_make__name').annotate(make_count=Count('id_car_make__name')) - vendor_bills=BillModel.objects.filter(vendor=vendor.vendor_model) - paginator=Paginator(vendor_bills,20) + vendor = get_object_or_404(models.Vendor, slug=slug, dealer=dealer) + cars = vendor.cars.all() + total_cars_from_vendor = cars.count() + vendor_makes = cars.values("id_car_make__name").annotate( + make_count=Count("id_car_make__name") + ) + vendor_bills = BillModel.objects.filter(vendor=vendor.vendor_model) + paginator = Paginator(vendor_bills, 20) page_number = request.GET.get("page") - page_obj=paginator.get_page(page_number) + page_obj = paginator.get_page(page_number) return render( - request, template_name="vendors/view_vendor.html", context={"vendor": vendor,"vendor_bills":page_obj,"total_cars_from_vendor":total_cars_from_vendor,"vendor_makes":vendor_makes} + request, + template_name="vendors/view_vendor.html", + context={ + "vendor": vendor, + "vendor_bills": page_obj, + "total_cars_from_vendor": total_cars_from_vendor, + "vendor_makes": vendor_makes, + }, ) @@ -2901,7 +3002,9 @@ class VendorCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - if vendor := models.Vendor.objects.filter(dealer=dealer,email=form.instance.email).first(): + if vendor := models.Vendor.objects.filter( + dealer=dealer, email=form.instance.email + ).first(): if not vendor.active: messages.error( self.request, @@ -3582,11 +3685,13 @@ class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) staff = models.Staff.objects.filter(dealer=dealer, active=True).all() return apply_search_filters(staff, query) + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context['no_staff_message']=_("staff") + context = super().get_context_data(**kwargs) + context["no_staff_message"] = _("staff") return context + class UserDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Represents a detailed view for displaying user-specific information. @@ -3667,7 +3772,10 @@ class UserCreateView( # return self.form_invalid(form) email = form.cleaned_data["email"] - if models.Staff.objects.filter(user__email=email).exists() or models.Dealer.objects.filter(user__email=email).exists(): + if ( + models.Staff.objects.filter(user__email=email).exists() + or models.Dealer.objects.filter(user__email=email).exists() + ): messages.error( self.request, _( @@ -3692,7 +3800,7 @@ class UserCreateView( # staff_member, _ = StaffMember.objects.get_or_create(user=user) # for service in form.cleaned_data["service_offered"]: - # staff_member.services_offered.add(service) + # staff_member.services_offered.add(service) staff.user = user staff.dealer = dealer staff.save() @@ -3702,7 +3810,9 @@ class UserCreateView( return super().form_valid(form) def get_success_url(self): - return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) + return reverse_lazy( + "staff_password_reset", args=[self.request.dealer.slug, self.staff_pk] + ) # return reverse_lazy("user_list", args=[self.request.dealer.slug]) @@ -3832,11 +3942,13 @@ class OrganizationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView organization = dealer.organizations.filter(active=True) return apply_search_filters(organization, query) + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context["empty_state_value"]=_("organization") + context = super().get_context_data(**kwargs) + context["empty_state_value"] = _("organization") return context + class OrganizationDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Handles displaying detailed information about an organization. @@ -3888,8 +4000,7 @@ class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) if organization := models.Organization.objects.filter( - dealer=dealer, - email=form.instance.email + dealer=dealer, email=form.instance.email ).first(): if not organization.active: messages.error( @@ -4218,7 +4329,7 @@ class BankAccountCreateView( def get_form(self, form_class=None): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) form = super().get_form(form_class) - account_qs = dealer.entity.get_get_default_coa_accounts().filter( + account_qs = dealer.entity.get_default_coa_accounts().filter( role__in=[ roles.ASSET_CA_CASH, roles.LIABILITY_CL_ACC_PAYABLE, @@ -4313,7 +4424,7 @@ class BankAccountUpdateView( def get_form(self, form_class=None): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) form = super().get_form(form_class) - account_qs = dealer.entity.get_get_default_coa_accounts().filter( + account_qs = dealer.entity.get_default_coa_accounts().filter( role__in=[ roles.ASSET_CA_CASH, roles.LIABILITY_CL_ACC_PAYABLE, @@ -4395,9 +4506,15 @@ class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_queryset(self): query = self.request.GET.get("q") dealer = get_user_type(self.request) - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) accounts = coa.get_coa_accounts() return apply_search_filters(accounts, query) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs @@ -4448,7 +4565,12 @@ class AccountCreateView( def form_valid(self, form): dealer = get_user_type(self.request) - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) form.instance.entity_model = dealer.entity form.instance.coa_model = coa form.instance.depth = 0 @@ -4458,31 +4580,47 @@ class AccountCreateView( def get_form_kwargs(self): dealer = get_user_type(self.request) kwargs = super().get_form_kwargs() - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) kwargs["coa_model"] = coa return kwargs def get_form(self, form_class=None): form = super().get_form(form_class) entity = get_user_type(self.request).entity - coa = ChartOfAccountModel.objects.get(entity=entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get(entity=entity, pk=self.kwargs["coa_pk"]) + or self.request.entity.get_default_coa() + ) form.initial["coa_model"] = coa return form def get_success_url(self): return reverse( - "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "coa_pk": self.kwargs["coa_pk"]} + "account_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "coa_pk": self.kwargs["coa_pk"], + }, ) - def get_context_data(self,**kwargs): + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: - context["coa_model"] = ChartOfAccountModel.objects.get(entity=self.request.entity,pk=coa_pk) + context["coa_model"] = ChartOfAccountModel.objects.get( + entity=self.request.entity, pk=coa_pk + ) except Exception: context["coa_model"] = self.request.entity.get_default_coa() return context + class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Represents the detailed view for an account with additional context data related to account @@ -4583,26 +4721,38 @@ class AccountUpdateView( return form def form_valid(self, form): - form.instance.coa_model = ChartOfAccountModel.objects.get(pk=self.kwargs['coa_pk']) or self.request.entity.get_default_coa() + form.instance.coa_model = ( + ChartOfAccountModel.objects.get(pk=self.kwargs["coa_pk"]) + or self.request.entity.get_default_coa() + ) return super().form_valid(form) def get_success_url(self): return reverse_lazy( - "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"coa_pk":self.kwargs["coa_pk"]} + "account_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "coa_pk": self.kwargs["coa_pk"], + }, ) - def get_context_data(self,**kwargs): + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: - context["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + context["coa_model"] = ( + ChartOfAccountModel.objects.get(pk=coa_pk) + or self.request.entity.get_default_coa() + ) except Exception: context["coa_model"] = self.request.entity.get_default_coa() return context + + @login_required @permission_required("django_ledger.delete_accountmodel") -def account_delete(request, dealer_slug,coa_pk, pk): +def account_delete(request, dealer_slug, coa_pk, pk): """ Handles the deletion of an account object identified by its primary key (pk). Ensures that the user has the necessary permissions to perform the deletion. Successfully @@ -4647,17 +4797,19 @@ def sales_list_view(request, dealer_slug): qs = [] try: if any([request.is_dealer, request.is_manager, request.is_accountant]): - qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders( + staff=staff, is_dealer=True, dealer=dealer + ) elif request.is_staff: - qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders(staff=staff, dealer=dealer) except Exception as e: print(e) - search_query = request.GET.get('q', None) + search_query = request.GET.get("q", None) if search_query: qs = qs.filter( - Q(order_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(customer__phone_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() paginator = Paginator(qs, 30) @@ -4747,11 +4899,13 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(models.Staff), - ).union(models.ExtraInfo.objects.filter( - dealer=dealer, - content_type=ContentType.objects.get_for_model(EstimateModel), - related_content_type=ContentType.objects.get_for_model(User), - )) + ).union( + models.ExtraInfo.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + ) + ) elif self.request.is_staff and self.request.is_sales: qs = models.ExtraInfo.objects.filter( @@ -4761,11 +4915,11 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): related_object_id=self.request.staff.pk, ) qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs]) - search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get("q", None) if search_query: qs = qs.filter( - Q(estimate_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() context["staff_estimates"] = qs return context @@ -4778,12 +4932,12 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): queryset = entity.get_estimates() if status: queryset = queryset.filter(status=status) - search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get("q", None) if search_query: queryset = queryset.filter( - Q(estimate_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() return queryset @@ -4820,7 +4974,9 @@ def create_estimate(request, dealer_slug, slug=None): data = json.loads(request.body) title = data.get("title") customer_id = data.get("customer") - customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first() + customer = models.Customer.objects.filter( + pk=int(customer_id), dealer=dealer + ).first() items = data.get("item", []) quantities = data.get("quantity", []) @@ -4911,7 +5067,9 @@ def create_estimate(request, dealer_slug, slug=None): "quantity": 1, "unit_cost": round(float(i.marked_price)), "unit_revenue": round(float(i.marked_price)), - "total_amount": round(float(i.final_price_plus_vat)),# TODO : check later + "total_amount": round( + float(i.final_price_plus_vat) + ), # TODO : check later } ) @@ -5055,9 +5213,13 @@ def create_estimate(request, dealer_slug, slug=None): ], "opportunity_id": slug if slug else None, "customer_count": entity.get_customers().count(), - "no_items_message": _("Please add at least one car or complete the car info before creating a quotation."), + "no_items_message": _( + "Please add at least one car or complete the car info before creating a quotation." + ), "no_items_button": _("Add car"), - "no_customers_message": _("Please add at least one customer before creating a quotation."), + "no_customers_message": _( + "Please add at least one customer before creating a quotation." + ), "no_customers_button": _("Add Customer"), } @@ -5092,22 +5254,45 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView def get_context_data(self, **kwargs): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) estimate = kwargs.get("object") + if estimate.get_itemtxs_data(): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["data"] = finance_data + kwargs["customer_obj"] = estimate.customer.customer_set.first() + kwargs["dealer_info"] = dealer kwargs["invoice"] = invoice_obj try: car = estimate.get_itemtxs_data()[0].first().item_model.car - selected_items = car.additional_services.filter(dealer=dealer) + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk + ) + try: + additionals = extra_info.data.get("additionals") + if additionals: + selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals) + else: + selected_items = [] + except Exception as e: + selected_items = [] + if estimate.is_draft() or estimate.is_review(): + kwargs["grand_total"] = finance_data.get("final_price") + sum([x.price_ for x in selected_items]) + else: + kwargs["grand_total"] = finance_data.get("grand_total") form = forms.AdditionalFinancesForm() - form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) # + form.fields["additional_finances"].queryset = form.fields[ + "additional_finances" + ].queryset.filter(dealer=dealer) # form.initial["additional_finances"] = selected_items kwargs["additionals_form"] = form + kwargs["additional_finances"] = selected_items + except Exception as e: logger.error(e) return super().get_context_data(**kwargs) @@ -5119,29 +5304,28 @@ class EstimatePrintView(EstimateDetailView): It reuses the data-fetching logic from EstimateDetailView but uses a dedicated, stripped-down print template. """ - template_name = "sales/estimates/estimate_preview.html" - + def get(self, request, *args, **kwargs): - self.object = self.get_object() context = self.get_context_data(object=self.object) - - + # lang = request.GET.get('lang', 'ar') - - - template_path = "sales/estimates/estimate_preview.html" - - + + if request.GET.get("lang") == "en": + template_path = "sales/estimates/estimate_preview_en.html" + else: + template_path = "sales/estimates/estimate_preview_ar.html" + html_string = render_to_string(template_path, context) - - - pdf_file = HTML(string=html_string).write_pdf() - - - response = HttpResponse(pdf_file, content_type='application/pdf') - response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' - + + base_url = request.build_absolute_uri("/") + pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() + + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' + ) + return response @@ -5196,8 +5380,10 @@ def create_sale_order(request, dealer_slug, pk): f"KeyError: 'car_info' or 'status' key missing when attempting to update status to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}." ) pass - item.item_model.car.sold_date=timezone.now() # to be checked added by faheed - item.item_model.car.save()# to be checked added byfaheed + item.item_model.car.sold_date = ( + timezone.now() + ) # to be checked added by faheed + item.item_model.car.save() # to be checked added byfaheed item.item_model.car.mark_as_sold() messages.success(request, "Sale Order created successfully") @@ -5218,7 +5404,7 @@ def create_sale_order(request, dealer_slug, pk): # form.fields["opportunity"].widget = HiddenInput() # calculator = CarFinanceCalculator(estimate) - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) return render( request, "sales/estimates/sale_order_form.html", @@ -5239,14 +5425,21 @@ def update_estimate_discount(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() discount_amount = request.POST.get("discount_amount", 0) - finance_data = get_finance_data(estimate,dealer) - car = finance_data.get('car') + finance_data = get_finance_data(estimate, dealer) + car = finance_data.get("car") if Decimal(discount_amount) >= car.marked_price: - messages.error(request, _("Discount amount cannot be greater than marked price")) + messages.error( + request, _("Discount amount cannot be greater than marked price") + ) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) - if Decimal(discount_amount) > car.marked_price * Decimal('0.5'): - messages.warning(request, _("Discount amount is greater than 50% of the marked price, proceed with caution.")) + if Decimal(discount_amount) > car.marked_price * Decimal("0.5"): + messages.warning( + request, + _( + "Discount amount is greater than 50% of the marked price, proceed with caution." + ), + ) else: messages.success(request, _("Discount updated successfully")) extra_info.data.update({"discount": Decimal(discount_amount)}) @@ -5263,9 +5456,17 @@ def update_estimate_additionals(request, dealer_slug, pk): if form.is_valid(): estimate = get_object_or_404(EstimateModel, pk=pk) car = estimate.get_itemtxs_data()[0].first().item_model.car - car.additional_services.set( - form.cleaned_data["additional_finances"] + additionals = form.cleaned_data["additional_finances"] + # car.additional_services.set(additionals) + additionals = [additional.pk for additional in additionals] + + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk, ) + extra_info.data.update({"additionals": additionals}) + extra_info.save() car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5291,7 +5492,7 @@ class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView): if estimate.get_itemtxs_data(): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) kwargs["data"] = finance_data return super().get_context_data(**kwargs) @@ -5389,10 +5590,9 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie def get_context_data(self, **kwargs): estimate = kwargs.get("object") if estimate.get_itemtxs_data(): - # data = get_financial_values(estimate) # calculator = CarFinanceCalculator(estimate) - kwargs["data"] = get_finance_data(estimate,self.request.dealer) + kwargs["data"] = get_finance_data(estimate, self.request.dealer) return super().get_context_data(**kwargs) @@ -5423,7 +5623,7 @@ def estimate_mark_as(request, dealer_slug, pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) estimate = get_object_or_404(EstimateModel, pk=pk) mark = request.GET.get("mark") - print(mark) + if mark: if mark == "review": if not estimate.can_review(): @@ -5443,6 +5643,24 @@ def estimate_mark_as(request, dealer_slug, pk): # Reserve The Car car = estimate.get_itemtxs_data()[0].first().item_model.car reserve_car(car, request) + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk + ) + try: + additionals = extra_info.data.get("additionals") + if additionals: + selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals) + else: + selected_items = [] + except Exception as e: + logger.error(e) + selected_items = [] + if selected_items: + car.additional_services.clear() + car.additional_services.set(selected_items) + messages.success(request, _("Quotation approved successfully")) return redirect("estimate_list", dealer_slug=dealer.slug) elif mark == "rejected": @@ -5522,7 +5740,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) entity = dealer.entity staff = getattr(self.request.user, "staff", None) - qs = [] + qs = None try: if any( [ @@ -5531,9 +5749,11 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): self.request.is_accountant, ] ): - qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_invoices( + staff=staff, is_dealer=True, dealer=dealer + ) elif self.request.is_staff: - qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_invoices(staff=staff, dealer=dealer) except Exception as e: print(e) @@ -5574,7 +5794,7 @@ class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) if invoice.get_itemtxs_data(): # calculator = CarFinanceCalculator(invoice) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(invoice,self.request.dealer) + finance_data = get_finance_data(invoice, self.request.dealer) kwargs["data"] = finance_data kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger @@ -5670,7 +5890,11 @@ class ApprovedInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "pk": self.object.pk, + }, ) @@ -5718,7 +5942,11 @@ class PaidInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "pk": self.object.pk, + }, ) def form_valid(self, form): @@ -5726,7 +5954,12 @@ class PaidInvoiceModelUpdateFormView( if invoice.get_amount_open() > 0: messages.error(self.request, "Invoice is not fully paid") - return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=self.kwargs["dealer_slug"], + entity_slug=self.kwargs["entity_slug"], + pk=invoice.pk, + ) else: invoice.post_ledger() invoice.save() @@ -5758,12 +5991,22 @@ def invoice_mark_as(request, dealer_slug, pk): if mark and mark == "accept": if not invoice.can_approve(): messages.error(request, "invoice is not ready for approval") - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) invoice.mark_as_approved( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin ) invoice.save() - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) @login_required @@ -5803,7 +6046,7 @@ def invoice_create(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) car = finance_data.get("car") invoice_itemtxs = { car.item_model.item_number: { @@ -5827,7 +6070,12 @@ def invoice_create(request, dealer_slug, pk): estimate.save() invoice.save() messages.success(request, "Invoice created successfully") - return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer.slug, + entity_slug=entity.slug, + pk=invoice.pk, + ) else: print(form.errors) form = forms.InvoiceModelCreateForm( @@ -5871,7 +6119,6 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView model = InvoiceModel context_object_name = "invoice" - template_name = "sales/invoices/invoice_preview.html" permission_required = ["django_ledger.view_invoicemodel"] def get_context_data(self, **kwargs): @@ -5879,16 +6126,44 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView invoice = kwargs.get("object") if invoice.get_itemtxs_data(): # calculator = CarFinanceCalculator(invoice) - finance_data = get_finance_data(invoice,dealer) + finance_data = get_finance_data(invoice, dealer) kwargs["data"] = finance_data - kwargs["dealer"] = dealer + kwargs["dealer_info"] = dealer + kwargs["customer_obj"] = invoice.customer.customer_set.first() return super().get_context_data(**kwargs) + def get(self, request, *args, **kwargs): + self.object = self.get_object() + context = self.get_context_data(object=self.object) + + # lang = request.GET.get('lang', 'ar') + + if request.GET.get("lang") == "en": + template_path = "sales/invoices/invoice_preview_en.html" + elif request.GET.get("lang") == "ar": + template_path = "sales/invoices/invoice_preview_ar.html" + else: + # just for preview not for download + return render(request, "sales/invoices/invoice_preview.html", context) + + html_string = render_to_string(template_path, context) + + base_url = request.build_absolute_uri("/") + pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() + + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="invoice_{self.object.invoice_number}.pdf"' + ) + + return response + # payments + class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): - template_name = 'sales/invoices/invoice_update.html' + template_name = "sales/invoices/invoice_update.html" permission_required = ["django_ledger.change_invoicemodel"] @@ -5914,6 +6189,7 @@ class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): # context = { "invoice": invoice, "form": form } # return render(request, "sales/payments/payment_form1.html", context) + @login_required @permission_required("inventory.add_payment", raise_exception=True) def PaymentCreateView(request, dealer_slug, pk): @@ -5960,7 +6236,12 @@ def PaymentCreateView(request, dealer_slug, pk): invoice = form.cleaned_data.get("invoice") # bill = form.cleaned_data.get("bill") payment_method = form.cleaned_data.get("payment_method") - response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail" + response = redirect( + "invoice_detail", + dealer_slug=dealer.slug, + entity_slug=entity.slug, + pk=model.pk, + ) # if invoice else "bill_detail" # model = invoice if invoice else bill if not model.is_approved(): @@ -6140,7 +6421,12 @@ def payment_mark_as_paid(request, dealer_slug, pk): exc_info=True, ) messages.error(request, f"Error: {str(e)}") - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) # activity log @@ -6221,10 +6507,10 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): | Q(last_name__icontains=query) | Q(id_car_make__name__icontains=query) | Q(id_car_model__name__icontains=query) - | Q(email__icontains=query) | Q(phone_number__icontains=query) | Q(next_action__icontains=query) - | Q(staff__name__icontains=query) + | Q(staff__first_name__icontains=query) + | Q(staff__last_name__icontains=query) ) if self.request.is_dealer: # or self.request.is_manager: @@ -6232,9 +6518,10 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if self.request.is_staff: return qs.filter(staff=self.request.staff) return models.Lead.objects.none() + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context['empty_state_value']=_("lead") + context = super().get_context_data(**kwargs) + context["empty_state_value"] = _("lead") return context @@ -6349,8 +6636,7 @@ def lead_create(request, dealer_slug): if instance.lead_type == "customer": customer = models.Customer.objects.filter( - dealer=dealer, - email=instance.email + dealer=dealer, email=instance.email ).first() if not customer: customer = models.Customer( @@ -6369,8 +6655,7 @@ def lead_create(request, dealer_slug): if instance.lead_type == "organization": organization = models.Organization.objects.filter( - dealer=dealer, - email=instance.email + dealer=dealer, email=instance.email ).first() if not organization: organization = models.Organization( @@ -6391,7 +6676,9 @@ def lead_create(request, dealer_slug): f"lead created successfully for dealer {dealer_slug} by user:{user_username}" ) messages.success(request, _("Lead created successfully")) - return redirect("lead_detail",dealer_slug=dealer_slug,slug=instance.slug) + return redirect( + "lead_detail", dealer_slug=dealer_slug, slug=instance.slug + ) else: logger.error( f"error creating leading for dealer {dealer_slug} by user:{user_username}" @@ -6409,13 +6696,11 @@ def lead_create(request, dealer_slug): form.filter_qs(dealer=dealer) if make := request.GET.get("id_car_make", None): - qs = models.CarModel.objects.filter( - id_car_make=int(make) - ) + qs = models.CarModel.objects.filter(id_car_make=int(make)) form.fields["id_car_model"].queryset = qs form.fields["id_car_model"].choices = [ (obj.id_car_model, obj.get_local_name()) for obj in qs - ] + ] else: dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list( @@ -6473,7 +6758,7 @@ def lead_tracking(request, dealer_slug): qs = models.Lead.objects.filter(dealer=dealer, staff=staff) else: qs = models.Lead.objects.filter(dealer=dealer) - leads=qs + leads = qs won = qs.filter(status="won") new = qs.filter(status="new") lose = qs.filter(status="lose") @@ -6486,7 +6771,7 @@ def lead_tracking(request, dealer_slug): "won": won, "lose": lose, "negotiation": negotiation, - "leads":leads, + "leads": leads, "empty_state_value": _("lead"), } return render(request, "crm/leads/lead_tracking.html", context) @@ -6518,10 +6803,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} submitted incomplete data to update lead actions " f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'." ) - messages.error( - request, - _("All fields are required") - ) + messages.error(request, _("All fields are required")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "All fields are required"}, status=400 @@ -6529,7 +6811,6 @@ def update_lead_actions(request, dealer_slug): # Get the lead - # Update lead fields lead.status = current_action @@ -6558,10 +6839,7 @@ def update_lead_actions(request, dealer_slug): f"submitted invalid date format ('{next_action_date}') " f"for Lead ID: {lead.pk}. Error: {ve}" ) - messages.error( - request, - _("Invalid date format") - ) + messages.error(request, _("Invalid date format")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "Invalid date format"}, status=400 @@ -6573,10 +6851,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). " f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'." ) - messages.success( - request, - _("Actions updated successfully") - ) + messages.success(request, _("Actions updated successfully")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": True, "message": "Actions updated successfully"} @@ -6588,10 +6863,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' " f"for dealer '{dealer_slug}'. Returning 404." ) - messages.error( - request, - _("Lead not found") - ) + messages.error(request, _("Lead not found")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": "Lead not found"}, status=404) except Exception as e: @@ -6601,27 +6873,23 @@ def update_lead_actions(request, dealer_slug): f"for dealer '{dealer_slug}'. Error: {e}", exc_info=True, # CRUCIAL: Includes the full traceback ) - messages.error( - request, - _("An error occurred while updating lead actions") - ) + messages.error(request, _("An error occurred while updating lead actions")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": str(e)}, status=500) -def lead_update(request,dealer_slug,slug): + +def lead_update(request, dealer_slug, slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) lead = get_object_or_404(models.Lead, slug=slug) form = forms.LeadForm(instance=lead) if "HX-Request" in request.headers: make_id = request.GET.get("id_car_make") make = models.CarMake.objects.get(pk=make_id) - form.fields[ - "id_car_model" - ].queryset = make.carmodel_set.all() + form.fields["id_car_model"].queryset = make.carmodel_set.all() else: form.fields[ - "id_car_model" - ].queryset = form.instance.id_car_make.carmodel_set.all() + "id_car_model" + ].queryset = form.instance.id_car_make.carmodel_set.all() form.fields["staff"].queryset = ( form.fields["staff"] .queryset.select_related("user") @@ -6631,10 +6899,9 @@ def lead_update(request,dealer_slug,slug): ) .distinct() ) - context = { - "form":form - } - return render(request,"crm/leads/lead_form.html",context) + context = {"form": form} + return render(request, "crm/leads/lead_form.html", context) + class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ @@ -6847,10 +7114,10 @@ def delete_note(request, dealer_slug, pk): note.delete() messages.success(request, _("Note deleted successfully.")) except Exception as e: - print("Errroooorrr: ",e) + print("Errroooorrr: ", e) print(url) print(dealer_slug) - return redirect(url, dealer_slug=dealer_slug,slug=slug) + return redirect(url, dealer_slug=dealer_slug, slug=slug) @login_required @@ -6941,7 +7208,7 @@ def schedule_event(request, dealer_slug, content_type, slug): form = forms.ScheduleForm(request.POST) if form.is_valid(): - reminder = form.cleaned_data['reminder'] + reminder = form.cleaned_data["reminder"] instance = form.save(commit=False) instance.dealer = dealer instance.content_object = obj @@ -6989,7 +7256,13 @@ def schedule_event(request, dealer_slug, content_type, slug): activity_type=instance.scheduled_type, ) if reminder: - scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at + scheduled_at_aware = ( + timezone.make_aware( + instance.scheduled_at, timezone.get_current_timezone() + ) + if timezone.is_naive(instance.scheduled_at) + else instance.scheduled_at + ) reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15) # Only schedule if the reminder time is in the future @@ -6997,15 +7270,17 @@ def schedule_event(request, dealer_slug, content_type, slug): if reminder_time > timezone.now(): DjangoQSchedule.objects.create( name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}", - func='inventory.tasks.send_schedule_reminder_email', + func="inventory.tasks.send_schedule_reminder_email", args=f'"{instance.pk}"', schedule_type=DjangoQSchedule.ONCE, next_run=reminder_time, - hook='inventory.tasks.log_email_status', + hook="inventory.tasks.log_email_status", ) messages.success(request, _("Appointment Created Successfully")) - return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug) + return redirect( + f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug + ) else: # Log for invalid form data @@ -7045,7 +7320,7 @@ def lead_transfer(request, dealer_slug, slug): messages.success(request, _("Lead transferred successfully")) else: messages.error(request, f"Invalid form data: {str(form.errors)}") - return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug) + return redirect("lead_detail", dealer_slug=dealer.slug, slug=lead.slug) @login_required @@ -7131,7 +7406,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # f"Lead's opportunity does not exist. Redirecting to lead list." # ) # return response - # return redirect("lead_list", dealer_slug=dealer.slug) + # return redirect("lead_list", dealer_slug=dealer.slug) if request.method == "POST": email_pk = request.POST.get("email_pk") @@ -7186,7 +7461,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # f"Lead's opportunity does not exist. Redirecting to lead list." # ) # return response - # return redirect("lead_list", dealer_slug=dealer_slug) + # return redirect("lead_list", dealer_slug=dealer_slug) msg = f""" السلام عليكم {lead.full_name}, @@ -7286,9 +7561,7 @@ class OpportunityCreateView( dealer=dealer, status="available", marked_price__gt=0 ) if self.request.is_dealer: - form.fields["lead"].queryset = models.Lead.objects.filter( - dealer=dealer - ) + form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer) elif self.request.is_staff: form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=self.request.staff @@ -7336,13 +7609,12 @@ class OpportunityUpdateView( success_message = _("Opportunity updated successfully.") permission_required = ["inventory.change_opportunity"] - def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) staff = getattr(self.request.user, "staff", None) form.fields["car"].queryset = models.Car.objects.filter( - dealer=dealer, status="available",marked_price__gt=0 + dealer=dealer, status="available", marked_price__gt=0 ) form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=staff @@ -7358,6 +7630,7 @@ class OpportunityUpdateView( }, ) + class OpportunityStageUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): @@ -7388,7 +7661,6 @@ class OpportunityStageUpdateView( success_message = _("Opportunity Stage updated successfully.") permission_required = ["inventory.change_opportunity"] - def get_success_url(self): return reverse_lazy( "opportunity_detail", @@ -7510,9 +7782,9 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) queryset = models.Opportunity.objects.filter(dealer=dealer) elif self.request.is_staff: staff = self.request.staff - queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) - - + queryset = models.Opportunity.objects.filter( + dealer=dealer, lead__staff=staff + ) # Stage filter stage = self.request.GET.get("stage") @@ -7529,7 +7801,7 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) elif sort == "closing": queryset = queryset.order_by("expected_close_date") - # Search filter + # Search filter search = self.request.GET.get("q") if search: queryset = queryset.filter( @@ -7647,14 +7919,13 @@ class NotificationListView(LoginRequiredMixin, ListView): return models.Notification.objects.filter(user=self.request.user) def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) user_notifications = self.get_queryset() # Calculate the number of total, read and unread notifications - context['total_count'] = user_notifications.count() - context['read_count'] = user_notifications.filter(is_read=True).count() - context['unread_count'] = user_notifications.filter(is_read=False).count() + context["total_count"] = user_notifications.count() + context["read_count"] = user_notifications.filter(is_read=True).count() + context["unread_count"] = user_notifications.filter(is_read=False).count() return context @@ -7797,12 +8068,14 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) query = self.request.GET.get("q") qs = models.AdditionalServices.objects.filter(dealer=dealer).all() if query: - qs = qs.filter(Q(name__icontains=query)| - Q(id__icontains=query)| - Q(uom__icontains=query) - ) + qs = qs.filter( + Q(name__icontains=query) + | Q(id__icontains=query) + | Q(uom__icontains=query) + ) return qs + class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.AdditionalServices template_name = "items/service/service_detail.html" @@ -7810,13 +8083,19 @@ class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = ["inventory.view_additionalservices"] def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - sold_cars=models.Car.objects.filter(status='sold',) - context['total_services_price']=self.object.price*self.object.additionals.filter(status='sold').count() + context = super().get_context_data(**kwargs) + sold_cars = models.Car.objects.filter( + status="sold", + ) + context["total_services_price"] = ( + self.object.price * self.object.additionals.filter(status="sold").count() + ) return context -class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class ItemExpenseCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ Represents a view for creating item expense entries. @@ -7907,9 +8186,6 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV ) - - - class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -7935,7 +8211,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) model = ItemModel template_name = "items/expenses/expenses_list.html" context_object_name = "expenses" - paginate_by =20 + paginate_by = 20 permission_required = ["django_ledger.view_itemmodel"] def get_queryset(self): @@ -7948,7 +8224,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): - queryset=ItemModel.objects.filter(item_role='expense') + queryset = ItemModel.objects.filter(item_role="expense") template_name = "items/expenses/expense_detail.html" context_object_name = "expense" permission_required = ["django_ledger.view_itemmodel"] @@ -7956,23 +8232,19 @@ class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Get the related bills queryset - bills_list = self.object.billmodel_set.all().order_by('-created') + bills_list = self.object.billmodel_set.all().order_by("-created") # Paginate the bills paginator = Paginator(bills_list, 10) # Show 10 bills per page - page_number = self.request.GET.get('page') + page_number = self.request.GET.get("page") page_obj = paginator.get_page(page_number) # Add the paginated bills to the context - context['page_obj'] = page_obj + context["page_obj"] = page_obj context["entity"] = get_user_type(self.request).entity return context - - - - class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Provides a view for listing bills. @@ -7995,7 +8267,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = BillModel template_name = "ledger/bills/bill_list.html" context_object_name = "bills" - paginate_by=20 + paginate_by = 20 permission_required = ["django_ledger.view_billmodel"] def get_queryset(self): @@ -8003,8 +8275,10 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): qs = dealer.entity.get_bills() query = self.request.GET.get("q") if query: - qs = qs.filter(Q(bill_number__icontains=query)| - Q(vendor__vendor_name__icontains=query)) + qs = qs.filter( + Q(bill_number__icontains=query) + | Q(vendor__vendor_name__icontains=query) + ) return qs def get_context_data(self, **kwargs): @@ -8013,7 +8287,9 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context -class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class BillModelCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") permission_required = "django_ledger.add_billmodel" @@ -8142,8 +8418,28 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMes return {"date_draft": get_localdate()} def get_form(self, form_class=None): + # form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - return BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs()) + form = BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs()) + form.initial["prepaid_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_prepaid_account + or None + ) + form.initial["unearned_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_unearned_account + or None + ) + form.initial["cash_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_cash_account + or None + ) + return form def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) @@ -8247,6 +8543,20 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMes }, ) + def get_queryset(self): + qs = super().get_queryset() + return qs.select_related( + "ledger", + "ledger__entity", + "vendor", + "cash_account", + "prepaid_account", + "unearned_account", + "cash_account__coa_model", + "prepaid_account__coa_model", + "unearned_account__coa_model", + ) + class BillModelDetailView(BillModelDetailViewBase): template_name = "bill/bill_detail.html" @@ -9298,7 +9608,9 @@ def DealerSettingsView(request, slug): form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer}) form.fields[ "invoice_cash_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_CASH) + ].queryset = dealer.entity.get_default_coa_accounts().filter( + role=roles.ASSET_CA_CASH + ) form.fields[ "invoice_prepaid_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( @@ -9321,21 +9633,23 @@ def DealerSettingsView(request, slug): ) form.fields[ "invoice_cost_of_good_sold_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter( - role=roles.COGS - ) + ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.COGS) form.fields[ "invoice_inventory_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( role=roles.ASSET_CA_INVENTORY ) - form.fields["bill_cash_account"].queryset = dealer.entity.get_default_coa_accounts().filter( + form.fields[ + "bill_cash_account" + ].queryset = dealer.entity.get_default_coa_accounts().filter( role=roles.ASSET_CA_CASH ) form.fields[ "bill_prepaid_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_PREPAID) + ].queryset = dealer.entity.get_default_coa_accounts().filter( + role=roles.ASSET_CA_PREPAID + ) form.fields[ "bill_unearned_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( @@ -9371,7 +9685,6 @@ def schedule_cancel(request, dealer_slug, pk): @login_required @permission_required("inventory.change_dealer", raise_exception=True) def assign_car_makes(request, dealer_slug): - """ Assigns car makes to a dealer. @@ -9499,7 +9812,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = "django_ledger.view_ledgermodel" -class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin): +class LedgerModelCreateView(LedgerModelCreateViewBase, SuccessMessageMixin): """ Handles the creation of LedgerModel entities. @@ -9563,16 +9876,17 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase): ) - @login_required @permission_required("django_ledger.delete_ledgermodel", raise_exception=True) -def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk): +def LedgerModelDeleteView(request, dealer_slug, entity_slug, ledger_pk): ledger = LedgerModel.objects.filter(pk=ledger_pk).first() if request.method == "POST": ledger.delete() messages.success(request, _("Ledger deleted successfully")) return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug) - return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger}) + return render(request, "ledger/ledger/ledger_delete.html", {"ledger_model": ledger}) + + # class LedgerModelDeleteView(DeleteView, SuccessMessageMixin): # """ # Handles the deletion of a Ledger model instance. @@ -9697,7 +10011,7 @@ class JournalEntryCreateView( @login_required @permission_required("django_ledger.delete_journalentrymodel", raise_exception=True) -def JournalEntryDeleteView(request,dealer_slug, pk): +def JournalEntryDeleteView(request, dealer_slug, pk): """ Handles the deletion of a specific journal entry. This view facilitates the deletion of a journal entry identified by its primary key (pk). If the @@ -9718,10 +10032,10 @@ def JournalEntryDeleteView(request,dealer_slug, pk): ledger = journal_entry.ledger if not journal_entry.can_delete(): messages.error(request, _("Journal Entry cannot be deleted")) - return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk) journal_entry.delete() messages.success(request, "Journal Entry deleted") - return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk) return render( request, "ledger/journal_entry/journal_entry_delete.html", @@ -9910,20 +10224,25 @@ def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk): @login_required @permission_required("inventory.change_dealer", raise_exception=True) def pricing_page(request, dealer_slug): - dealer=get_object_or_404(models.Dealer, slug=dealer_slug) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) vat = models.VatRate.objects.filter(dealer=dealer).first() now = datetime.now().date() + timedelta(days=15) - if not hasattr(dealer.user,'userplan') or dealer.is_plan_expired or dealer.user.userplan.expire <= now: - plan_list = PlanPricing.objects.annotate( - price_with_tax=Round(F('price') * vat.rate + F('price'), 2) - ).all() + if ( + not hasattr(dealer.user, "userplan") + or dealer.is_plan_expired + or dealer.user.userplan.expire <= now + ): + plan_list = PlanPricing.objects.annotate( + price_with_tax=Round(F("price") * vat.rate + F("price"), 2) + ).all() - form = forms.PaymentPlanForm() - return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) + form = forms.PaymentPlanForm() + return render( + request, "pricing_page.html", {"plan_list": plan_list, "form": form} + ) else: - messages.info(request,_("You already have an plan!!")) - return redirect('home',dealer_slug=dealer_slug) - + messages.info(request, _("You already have an plan!!")) + return redirect("home", dealer_slug=dealer_slug) # @login_required @@ -9972,11 +10291,11 @@ def submit_plan(request, dealer_slug): return redirect("pricing_page", dealer_slug=dealer_slug) # Store plan & dealer info in session for use in callback - request.session['pending_plan_id'] = selected_plan_id - request.session['pending_dealer_slug'] = dealer_slug + request.session["pending_plan_id"] = selected_plan_id + request.session["pending_dealer_slug"] = dealer_slug # Initiate payment WITHOUT creating order - transaction_url,error = handle_payment(request, dealer) + transaction_url, error = handle_payment(request, dealer) if not transaction_url: messages.error(request, _(f"Payment initiation failed. {error}")) return redirect("pricing_page", dealer_slug=dealer_slug) @@ -9986,25 +10305,52 @@ def submit_plan(request, dealer_slug): # @login_required def payment_callback(request, dealer_slug): + from django.db import transaction + payment_id = request.GET.get("id") payment_status = request.GET.get("status") message = request.GET.get("message", "") - logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") + if not payment_id: + logger.error("Missing payment ID in callback") + return render(request, "payment_failed.html", {"message": "Invalid request"}) - history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() - if not history: - logger.error(f"No PaymentHistory found for transaction_id: {payment_id}") - return render(request, "payment_failed.html", {"message": "Invalid transaction"}) + logger.info( + f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}" + ) - if history.status == "paid": - logger.info("Payment already processed. Redirecting to home.") - return redirect('home') + with transaction.atomic(): + history = ( + models.PaymentHistory.objects + .select_for_update() + .filter(transaction_id=payment_id) + .first() + ) + + if not history: + logger.error(f"No PaymentHistory found for transaction_id: {payment_id}") + return render( + request, "payment_failed.html", {"message": "Invalid transaction"} + ) + + if history.status == "paid": + logger.info("Payment already processed. Redirecting to home.") + return redirect("home") + + if history.status == "processing": + logger.warning(f"Payment {payment_id} is already being processed. Skipping.") + return redirect("home") + + if history.status == "failed" and payment_status != "paid": + logger.warning(f"Payment {payment_id} already failed. Ignoring.") + return render(request, "payment_failed.html", {"message": message or "Payment failed"}) + + history.status = "processing" + history.save(update_fields=["status"]) if payment_status == "paid": logger.info(f"Payment successful for transaction ID {payment_id}. Creating order...") - # Get metadata from PaymentHistory (passed during handle_payment) metadata = history.user_data if isinstance(metadata, str): try: @@ -10012,107 +10358,102 @@ def payment_callback(request, dealer_slug): except json.JSONDecodeError: logger.error(f"Failed to decode metadata JSON: {metadata}") metadata = {} + plan_pricing_id = metadata.get("plan_pricing_id") dealer_slug_from_meta = metadata.get("dealer_slug") if not plan_pricing_id or dealer_slug_from_meta != dealer_slug: logger.error("Invalid metadata in payment callback") history.status = "failed" - history.save() + history.save(update_fields=["status"]) return render(request, "payment_failed.html", {"message": "Invalid payment data"}) - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - pp = get_object_or_404(PlanPricing, pk=plan_pricing_id) - - # ✅ CREATE ORDER HERE try: - order = Order.objects.create( - user=dealer.user, - plan=pp.plan, - pricing=pp.pricing, - amount=pp.price, - currency="SAR", # Fixed typo: was "SA" - tax=15, - status=Order.STATUS.NEW, # Use constant if available - ) - logger.info(f"Order {order.id} created for user {dealer.user}") - except Exception as e: - logger.exception(f"Failed to create order: {e}") - history.status = "failed" - history.save() - return render(request, "payment_failed.html", {"message": "Order creation failed"}) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + pp = get_object_or_404(PlanPricing, pk=plan_pricing_id) - # Create or get BillingInfo - billing_info, created = BillingInfo.objects.get_or_create( - user=dealer.user, - defaults={ - 'tax_number': dealer.vrn, - 'name': dealer.arabic_name, - 'street': dealer.address, - 'zipcode': dealer.entity.zip_code or " ", - 'city': dealer.entity.city or " ", - 'country': dealer.entity.country or " ", - } - ) - if created: - logger.info(f"Created new billing info for user {dealer.user}.") - else: - logger.debug(f"Billing info already exists for user {dealer.user}.") + with transaction.atomic(): + history.refresh_from_db() + if history.status == "paid": + logger.info("Payment was already completed by another request. Skipping.") + return redirect("home") - # Create or update UserPlan - if not hasattr(order.user, 'userplan'): - UserPlan.objects.create( - user=order.user, - plan=order.plan, - # expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) - ) - logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") - else: - # Optional: upgrade existing plan - # user_plan = order.user.userplan - # user_plan.plan = order.plan - # user_plan.save() - logger.info(f"UserPlan already exists for user {order.user}.") + order = Order.objects.create( + user=dealer.user, + plan=pp.plan, + pricing=pp.pricing, + amount=pp.price, + currency="SAR", + tax=15, + status=Order.STATUS.NEW, + ) + logger.info(f"Order {order.id} created for user {dealer.user}") - try: - # Complete the order (this may generate invoice, etc.) - order.complete_order() - history.status = "paid" - history.order = order # Link payment to order - history.save() + billing_info, created = BillingInfo.objects.get_or_create( + user=dealer.user, + defaults={ + "tax_number": dealer.vrn, + "name": dealer.arabic_name, + "street": dealer.address, + "zipcode": dealer.entity.zip_code or " ", + "city": dealer.entity.city or " ", + "country": dealer.entity.country or " ", + }, + ) + + # Create UserPlan if missing + if not hasattr(order.user, "userplan"): + UserPlan.objects.create( + user=order.user, + plan=order.plan, + ) + logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") + else: + logger.info(f"UserPlan already exists for user {order.user}.") + + order.complete_order() + + history.status = "paid" + history.order = order + history.save(update_fields=["status"]) invoice = order.get_invoices().first() - - logger.info(f"Order {order.id} completed. Rendering success page.") + logger.info(f"Order {order.id} completed successfully.") return render( - request, - "payment_success.html", - {"order": order, "invoice": invoice} + request, "payment_success.html", {"order": order, "invoice": invoice} ) except Exception as e: - logger.exception(f"Error completing order {order.id}: {e}") + logger.exception(f"Error processing paid payment {payment_id}: {e}") + # Mark as failed history.status = "failed" - history.save() - return render(request, "payment_failed.html", {"message": "Plan activation error"}) + history.save(update_fields=["status"]) + return render(request, "payment_failed.html", {"message": "Payment processing error"}) finally: - # Activate dealer & staff if needed - if dealer := getattr(order.user, "dealer", None): - if not dealer.user.is_active: - dealer.user.is_active = True - dealer.user.save() - for staff in dealer.get_staff(): - if not staff.user.is_active: - staff.activate_account() + try: + if dealer := getattr(order.user, "dealer", None): + if not dealer.user.is_active: + dealer.user.is_active = True + dealer.user.save() + for staff in dealer.get_staff(): + if not staff.user.is_active: + staff.activate_account() + except Exception as ex: + logger.warning(f"Failed to activate dealer/staff: {ex}") elif payment_status == "failed": logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") history.status = "failed" - history.save() + history.save(update_fields=["status"]) return render(request, "payment_failed.html", {"message": message}) - return render(request, "payment_failed.html", {"message": "Unknown payment status"}) + else: + logger.warning(f"Unknown payment status: {payment_status}") + history.status = "failed" + history.save(update_fields=["status"]) + return render(request, "payment_failed.html", {"message": "Unknown payment status"}) + # @login_required # @permission_required("inventory.change_dealer", raise_exception=True) # def payment_callback(request, dealer_slug): @@ -10237,8 +10578,11 @@ def payment_callback(request, dealer_slug): # return render(request, "payment_failed.html", {"message": message}) + @login_required -async def sse_stream(request): # 👈 Mark as async! +async def sse_stream(request): + import asyncio + def event_generator(): last_id = int(request.GET.get("last_id", 0)) @@ -10246,11 +10590,13 @@ async def sse_stream(request): # 👈 Mark as async! async def fetch_notifications(): while True: # 🔥 Fully async ORM query - notifications = models.Notification.objects.filter( - user=request.user, - id__gt=last_id, - is_read=False - ).order_by("created").values("id", "message", "created") + notifications = ( + models.Notification.objects.filter( + user=request.user, id__gt=last_id, is_read=False + ) + .order_by("created") + .values("id", "message", "created") + ) # 🔥 Async iteration over queryset async for notification in notifications: @@ -10280,7 +10626,7 @@ async def sse_stream(request): # 👈 Mark as async! "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", - } + }, ) @@ -10432,7 +10778,6 @@ def add_task(request, dealer_slug, content_type, slug): return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug) - @login_required @permission_required("inventory.change_tasks", raise_exception=True) def update_task(request, dealer_slug, pk): @@ -10547,10 +10892,12 @@ def management_view(request, dealer_slug): def user_management(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) context = { - "customers": models.Customer.objects.filter(active=False,dealer=dealer), - "organizations": models.Organization.objects.filter(active=False,dealer=dealer), - "vendors": models.Vendor.objects.filter(active=False,dealer=dealer), - "staff": models.Staff.objects.filter(active=False,dealer=dealer), + "customers": models.Customer.objects.filter(active=False, dealer=dealer), + "organizations": models.Organization.objects.filter( + active=False, dealer=dealer + ), + "vendors": models.Vendor.objects.filter(active=False, dealer=dealer), + "staff": models.Staff.objects.filter(active=False, dealer=dealer), } return render(request, "admin_management/user_management.html", context) @@ -10878,19 +11225,23 @@ def InventoryItemCreateView(request, dealer_slug): serie = request.POST.get("serie") trim = request.POST.get("trim") year = request.POST.get("year") - exterior = models.ExteriorColors.objects.get( - pk=request.POST.get("exterior") - ) - interior = models.InteriorColors.objects.get( - pk=request.POST.get("interior") - ) + exterior = request.POST.get("exterior") + interior = request.POST.get("interior") make_name = models.CarMake.objects.get(pk=make) model_name = models.CarModel.objects.get(pk=model) serie_name = models.CarSerie.objects.get(pk=serie) trim_name = models.CarTrim.objects.get(pk=trim) + exterior_name = models.ExteriorColors.objects.get( + pk=request.POST.get("exterior") + ) + interior_name = models.InteriorColors.objects.get( + pk=request.POST.get("interior") + ) + + inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior_name.name} || {interior_name.name}" + display_name = f"{make_name.name} {model_name.name} {serie_name.name} {trim_name.name} {year} {exterior_name.name}" - inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior.name} || {interior.name}" if ( inventory := entity.get_items_inventory() .filter(name=inventory_name) @@ -10898,17 +11249,28 @@ def InventoryItemCreateView(request, dealer_slug): ): messages.error(request, _("Inventory item already exists")) return response - uom = entity.get_uom_all().filter(name="Unit").first() if not uom: uom = entity.create_uom(name="Unit", unit_abbr="unit") - entity.create_item_inventory( - name=inventory_name, + item = entity.create_item_inventory( + name=display_name, uom_model=uom, item_type=ItemModel.ITEM_TYPE_MATERIAL, inventory_account=account, coa_model=coa, ) + item.additional_info.update( + { + "make": make, + "model": model, + "serie": serie, + "trim": trim, + "year": year, + "exterior": exterior, + "interior": interior, + } + ) + item.save() messages.success(request, _("Inventory item created successfully")) return response @@ -10992,7 +11354,13 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai title = f"Purchase Order {po_model.po_number}" context["page_title"] = title context["header_title"] = title - context["po_ready_to_fulfill"] = all([item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']) + context["po_ready_to_fulfill"] = all( + [ + item + for item in po_model.get_itemtxs_data()[0] + if item.po_item_status == "received" + ] + ) po_model: PurchaseOrderModel = self.object po_items_qs, item_data = po_model.get_itemtxs_data( queryset=po_model.itemtransactionmodel_set.all().select_related( @@ -11005,7 +11373,14 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai for i in po_items_qs.values("po_total_amount", "po_item_status") if i["po_item_status"] != "cancelled" ) + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po_model.get_itemtxs_data()[0].all() + ] + po_quantity = sum(item["q"] for item in items) + context["po_quantity"] = po_quantity return context + def get(self, request, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) @@ -11014,38 +11389,37 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai "item_model", "bill_model" ) ) - - if self.object.po_status == 'fulfilled': - context['po_items_list']=po_items_qs - context['vendor']=po_items_qs.first().bill_model.vendor - context['dealer']=request.dealer - + + if self.object.po_status == "fulfilled": + context["po_items_list"] = po_items_qs + context["vendor"] = po_items_qs.first().bill_model.vendor + context["dealer"] = request.dealer + # Check if PDF format is requested - if request.GET.get('format') == 'pdf': + if request.GET.get("format") == "pdf": # Use a separate, print-friendly template for the PDF - if request.GET.get('lang')=='en': + if request.GET.get("lang") == "en": html_string = render_to_string( - "purchase_orders/po_detail_en_pdf.html", - context - ) - else: - html_string=render_to_string( - "purchase_orders/po_detail_ar_pdf.html", - context + "purchase_orders/po_detail_en_pdf.html", context ) - - - - # Use WeasyPrint to generate the PDF - pdf = HTML(string=html_string).write_pdf() - + else: + html_string = render_to_string( + "purchase_orders/po_detail_ar_pdf.html", context + ) + + base_url = request.build_absolute_uri("/") + pdf = HTML(string=html_string, base_url=base_url).write_pdf() + response = HttpResponse(pdf, content_type="application/pdf") - response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' + response["Content-Disposition"] = ( + f'attachment; filename="PO_{self.object.po_number}.pdf"' + ) return response # If not a PDF request, return the standard HTML response return self.render_to_response(context) + class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = PurchaseOrderModel context_object_name = "purchase_orders" @@ -11058,14 +11432,18 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie query = self.request.GET.get("q") qs = self.model.objects.filter(entity=dealer.entity) if query: - qs=qs.filter(Q(po_number__icontains=query)|Q(po_status__icontains=query)|Q(po_title__icontains=query)) + qs = qs.filter( + Q(po_number__icontains=query) + | Q(po_status__icontains=query) + | Q(po_title__icontains=query) + ) return qs return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) dealer = get_user_type(self.request) - vendors=models.Vendor.objects.filter(dealer=dealer) + vendors = models.Vendor.objects.filter(dealer=dealer) context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug context["vendors"] = vendors @@ -11083,7 +11461,6 @@ class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): - template_name = "purchase_orders/po_delete.html" permission_required = "django_ledger.delete_purchaseordermodel" def get_success_url(self): @@ -11093,7 +11470,11 @@ class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): level=messages.SUCCESS, ) return reverse( - "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]} + "purchase_order_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + }, ) @@ -11205,7 +11586,9 @@ def upload_cars(request, dealer_slug, pk=None): response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk) if po_item.status == "uploaded": - messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.") + messages.add_message( + request, messages.SUCCESS, "Item uploaded Sucessfully." + ) return redirect( "view_items_inventory", dealer_slug=dealer_slug, @@ -11223,18 +11606,18 @@ def upload_cars(request, dealer_slug, pk=None): ) try: if item: - data = [x.strip() for x in item.item_model.name.split("||")] - make = models.CarMake.objects.filter(is_sa_import=True).get( - name=data[0] + # data = [x.strip() for x in item.item_model.name.split("||")] + make = models.CarMake.objects.get(pk=item.item_model.additional_info.get("make")) + model = models.CarModel.objects.get(pk=item.item_model.additional_info.get("model")) + trim = models.CarTrim.objects.get(pk=item.item_model.additional_info.get("trim")) + serie = models.CarSerie.objects.get(pk=item.item_model.additional_info.get("serie")) + year = item.item_model.additional_info.get("year") + exterior = models.ExteriorColors.objects.get( + pk=item.item_model.additional_info.get("exterior") + ) + interior = models.InteriorColors.objects.get( + pk=item.item_model.additional_info.get("interior") ) - model = make.carmodel_set.get(name=data[1]) - trim = models.CarTrim.objects.filter( - name=data[3], id_car_serie__id_car_model=model.id_car_model - ).first() - serie = trim.id_car_serie - year = data[4] - exterior = models.ExteriorColors.objects.get(name=data[5]) - interior = models.InteriorColors.objects.get(name=data[6]) receiving_date = timezone.now() vendor_model = item.bill_model.vendor vendor = models.Vendor.objects.get(vendor_model=vendor_model) @@ -11421,23 +11804,24 @@ class InventoryListView(InventoryListViewBase): template_name = "inventory/list.html" permission_required = ["django_ledger.view_purchaseordermodel"] + @login_required def purchase_report_view(request, dealer_slug): - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") pos = request.entity.get_purchase_orders() if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__lte=end_date) except (ValueError, TypeError): pass @@ -11447,7 +11831,10 @@ def purchase_report_view(request, dealer_slug): total_po_cars = 0 for po in pos: - items = [{"total": x.total_amount, "q": x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po.get_itemtxs_data()[0].all() + ] po_amount = sum(item["total"] for item in items) po_quantity = sum(item["q"] for item in items) @@ -11459,15 +11846,17 @@ def purchase_report_view(request, dealer_slug): vendors = set([bill.vendor.vendor_name for bill in bills]) vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" - data.append({ - "po_number": po.po_number, - "po_created": po.created, - "po_status": po.po_status, - "po_fulfilled_date": po.date_fulfilled, - "po_amount": po_amount, - "po_quantity": po_quantity, - "vendors_str": vendors_str - }) + data.append( + { + "po_number": po.po_number, + "po_created": po.created, + "po_status": po.po_status, + "po_fulfilled_date": po.date_fulfilled, + "po_amount": po_amount, + "po_quantity": po_quantity, + "vendors_str": vendors_str, + } + ) current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") context = { @@ -11481,42 +11870,42 @@ def purchase_report_view(request, dealer_slug): "end_date": end_date_str, } - return render(request, 'ledger/reports/purchase_report.html', context) + return render(request, "ledger/reports/purchase_report.html", context) def purchase_report_csv_export(request, dealer_slug): - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type="text/csv") current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S") filename = f"purchase_report_{dealer_slug}_{current_time}.csv" - response['Content-Disposition'] = f'attachment; filename="{filename}"' + response["Content-Disposition"] = f'attachment; filename="{filename}"' writer = csv.writer(response) header = [ - 'PO Number', - 'Created Date', - 'Status', - 'Fulfilled Date', - 'PO Amount', - 'PO Quantity', - 'Vendors' + "PO Number", + "Created Date", + "Status", + "Fulfilled Date", + "PO Amount", + "PO Quantity", + "Vendors", ] writer.writerow(header) pos = request.entity.get_purchase_orders() - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__lte=end_date) except (ValueError, TypeError): pass @@ -11524,7 +11913,10 @@ def purchase_report_csv_export(request, dealer_slug): for po in pos: po_amount = 0 po_quantity = 0 - items = [{"total": x.total_amount, "q": x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po.get_itemtxs_data()[0].all() + ] for item in items: po_amount += item["total"] @@ -11534,15 +11926,17 @@ def purchase_report_csv_export(request, dealer_slug): vendors = set([bill.vendor.vendor_name for bill in bills]) vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" - writer.writerow([ - po.po_number, - po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', - po.get_po_status_display(), - po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '', - f"{po_amount:.2f}", - po_quantity, - vendors_str - ]) + writer.writerow( + [ + po.po_number, + po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else "", + po.get_po_status_display(), + po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else "", + f"{po_amount:.2f}", + po_quantity, + vendors_str, + ] + ) return response @@ -11552,16 +11946,16 @@ def car_sale_report_view(request, dealer_slug): vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() VAT_RATE = vat.rate if vat else 0 - cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + cars_sold = models.Car.objects.filter(dealer=dealer, status="sold") # Get filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") # Apply filters to the queryset if selected_make: @@ -11578,81 +11972,96 @@ def car_sale_report_view(request, dealer_slug): # Corrected: Apply date filters using the 'sold_date' field if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass # Calculate summary data for the filtered results total_cars_sold = cars_sold.count() - total_revenue_from_cars = cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_revenue_from_cars = ( + cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_on_cars = cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_on_cars = ( + cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) - total_revenue_from_additonals = sum([car.get_additional_services()['total'] for car in cars_sold]) - total_vat_from_additonals = sum([car.get_additional_services()['services_vat'] for car in cars_sold]) + total_revenue_from_additonals = sum( + [car.get_additional_services()["total"] for car in cars_sold] + ) + total_vat_from_additonals = sum( + [car.get_additional_services()["services_vat"] for car in cars_sold] + ) total_vat_collected = total_vat_on_cars + total_vat_from_additonals total_revenue_collected = total_revenue_from_cars + total_revenue_from_additonals - total_discount = cars_sold.aggregate(total=Sum('discount_amount'))['total'] or 0 + total_discount = cars_sold.aggregate(total=Sum("discount_amount"))["total"] or 0 current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") # Get distinct makes for the initial dropdown, other dropdowns will be populated via AJAX - base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status='sold') - makes = base_sold_cars_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') + base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status="sold") + makes = ( + base_sold_cars_queryset.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) context = { - 'cars_sold': cars_sold, - 'total_cars_sold': total_cars_sold, - 'current_time': current_time, - 'dealer': dealer, - 'total_revenue_from_cars': total_revenue_from_cars, - 'total_revenue_from_additonals': total_revenue_from_additonals, - 'total_revenue_collected': total_revenue_collected, - 'total_vat_on_cars': total_vat_on_cars, - 'total_vat_from_additonals': total_vat_from_additonals, - 'total_vat_collected': total_vat_collected, - 'total_discount': total_discount, - 'makes': makes, - 'selected_make': selected_make, - 'selected_model': selected_model, - 'selected_serie': selected_serie, - 'selected_year': selected_year, - 'selected_stock_type': selected_stock_type, - 'start_date': start_date_str, - 'end_date': end_date_str, + "cars_sold": cars_sold, + "total_cars_sold": total_cars_sold, + "current_time": current_time, + "dealer": dealer, + "total_revenue_from_cars": total_revenue_from_cars, + "total_revenue_from_additonals": total_revenue_from_additonals, + "total_revenue_collected": total_revenue_collected, + "total_vat_on_cars": total_vat_on_cars, + "total_vat_from_additonals": total_vat_from_additonals, + "total_vat_collected": total_vat_collected, + "total_discount": total_discount, + "makes": makes, + "selected_make": selected_make, + "selected_model": selected_model, + "selected_serie": selected_serie, + "selected_year": selected_year, + "selected_stock_type": selected_stock_type, + "start_date": start_date_str, + "end_date": end_date_str, } - return render(request, 'ledger/reports/car_sale_report.html', context) + return render(request, "ledger/reports/car_sale_report.html", context) ### 2. Updated `get_filtered_choices` + @login_required def get_filtered_choices(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) # Get all filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") # Start with the base queryset - queryset = models.Car.objects.filter(dealer=dealer, status='sold') + queryset = models.Car.objects.filter(dealer=dealer, status="sold") # Apply filters based on what is selected if selected_make: @@ -11667,59 +12076,88 @@ def get_filtered_choices(request, dealer_slug): # Corrected: Apply date filters to the AJAX queryset if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() queryset = queryset.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() queryset = queryset.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass data = { - 'models': list(queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name')), - 'series': list(queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name')), - 'years': list(queryset.values_list('year', flat=True).distinct().order_by('-year')), - 'stock_types': list(queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type')) + "models": list( + queryset.values_list("id_car_model__name", flat=True) + .distinct() + .order_by("id_car_model__name") + ), + "series": list( + queryset.values_list("id_car_serie__name", flat=True) + .distinct() + .order_by("id_car_serie__name") + ), + "years": list( + queryset.values_list("year", flat=True).distinct().order_by("-year") + ), + "stock_types": list( + queryset.values_list("stock_type", flat=True) + .distinct() + .order_by("stock_type") + ), } return JsonResponse(data) ### 3. Updated `car_sale_report_csv_export` + @login_required def car_sale_report_csv_export(request, dealer_slug): - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type="text/csv") current_time = timezone.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"sales_report_{dealer_slug}_{current_time}.csv" - response['Content-Disposition'] = f'attachment; filename="{filename}"' + response["Content-Disposition"] = f'attachment; filename="{filename}"' writer = csv.writer(response) # Define the CSV header based on your HTML table headers header = [ - 'VIN', 'Make', 'Model', 'Year', 'Serie', 'Trim', 'Mileage', - 'Stock Type', 'Created Date', 'Sold Date', 'Cost Price', - 'Marked Price', 'Discount Amount', 'Selling Price', - 'VAT on Car', 'Services Price', 'VAT on Services', 'Final Total', - 'Invoice Number' + "VIN", + "Make", + "Model", + "Year", + "Serie", + "Trim", + "Mileage", + "Stock Type", + "Created Date", + "Sold Date", + "Cost Price", + "Marked Price", + "Discount Amount", + "Selling Price", + "VAT on Car", + "Services Price", + "VAT on Services", + "Final Total", + "Invoice Number", ] writer.writerow(header) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + cars_sold = models.Car.objects.filter(dealer=dealer, status="sold") # Apply filters from the request, just like in your HTML view - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if selected_make: cars_sold = cars_sold.filter(id_car_make__name=selected_make) @@ -11735,14 +12173,14 @@ def car_sale_report_csv_export(request, dealer_slug): # Corrected: Apply date filters for CSV export if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass @@ -11750,8 +12188,8 @@ def car_sale_report_csv_export(request, dealer_slug): # Write the data for the filtered cars for car in cars_sold: additional_services = car.get_additional_services() - services_total_price = additional_services['total'] - services_vat_amount = additional_services['services_vat'] + services_total_price = additional_services["total"] + services_vat_amount = additional_services["services_vat"] invoice_number = None sold_date = None @@ -11759,124 +12197,142 @@ def car_sale_report_csv_export(request, dealer_slug): invoice_number = car.invoice.invoice_number sold_date = car.invoice.date_paid - writer.writerow([ - car.vin, - car.id_car_make.name, - car.id_car_model.name, - car.year, - car.id_car_serie.name, - car.id_car_trim.name, - car.mileage if car.mileage else '0', - car.stock_type, - car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', - sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else '', - car.cost_price, - car.marked_price, - car.discount_amount, - car.final_price, - car.vat_amount, - services_total_price, - services_vat_amount, - car.final_price_plus_services_plus_vat, - invoice_number, - ]) + writer.writerow( + [ + car.vin, + car.id_car_make.name, + car.id_car_model.name, + car.year, + car.id_car_serie.name, + car.id_car_trim.name, + car.mileage if car.mileage else "0", + car.stock_type, + car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else "", + sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else "", + car.cost_price, + car.marked_price, + car.discount_amount, + car.final_price, + car.vat_amount, + services_total_price, + services_vat_amount, + car.final_price_plus_services_plus_vat, + invoice_number, + ] + ) return response + @login_required # @permission_required('inventory.view_staff') def staff_password_reset_view(request, dealer_slug, user_pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first() - if request.method == 'POST': + if request.method == "POST": form = forms.CustomSetPasswordForm(staff.user, request.POST) if form.is_valid(): form.save() - messages.success(request, _('Your password has been set. You may go ahead and log in now.')) - return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug) + messages.success( + request, + _("Your password has been set. You may go ahead and log in now."), + ) + return redirect("user_detail", dealer_slug=dealer_slug, slug=staff.slug) else: - messages.error(request, _(f'Invalid password. {str(form.errors)}')) + messages.error(request, _(f"Invalid password. {str(form.errors)}")) form = forms.CustomSetPasswordForm(staff.user) - return render(request, 'users/user_password_reset.html', {'form': form}) + return render(request, "users/user_password_reset.html", {"form": form}) + class RecallListView(ListView): model = models.Recall - template_name = 'recalls/recall_list.html' - context_object_name = 'recalls' + template_name = "recalls/recall_list.html" + context_object_name = "recalls" paginate_by = 20 def get_queryset(self): - queryset = super().get_queryset().annotate( - dealer_count=Count('notifications', distinct=True), - car_count=Count('notifications__cars_affected', distinct=True) + queryset = ( + super() + .get_queryset() + .annotate( + dealer_count=Count("notifications", distinct=True), + car_count=Count("notifications__cars_affected", distinct=True), + ) ) - return queryset.select_related('make', 'model', 'serie', 'trim') + return queryset.select_related("make", "model", "serie", "trim") class RecallDetailView(DetailView): model = models.Recall - template_name = 'recalls/recall_detail.html' - context_object_name = 'recall' + template_name = "recalls/recall_detail.html" + context_object_name = "recall" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notifications'] = self.object.notifications.select_related('dealer') + context["notifications"] = self.object.notifications.select_related("dealer") return context def RecallFilterView(request): - context = {'make_data': models.CarMake.objects.all()} + context = {"make_data": models.CarMake.objects.all()} if request.method == "POST": - make = request.POST.get('make') - model = request.POST.get('model') - serie = request.POST.get('serie') - trim = request.POST.get('trim') - year = request.POST.get('year') - url = reverse('recall_create') + make = request.POST.get("make") + model = request.POST.get("model") + serie = request.POST.get("serie") + trim = request.POST.get("trim") + year = request.POST.get("year") + url = reverse("recall_create") url += f"?make={make}&model={model}&serie={serie}&trim={trim}&year={year}" - cars = models.Car.objects.filter(id_car_make=make,id_car_model=model,id_car_serie=serie,id_car_trim=trim,year=year) - context['url'] = url - context['cars'] = cars - return render(request,'recalls/recall_filter.html',context) + cars = models.Car.objects.filter( + id_car_make=make, + id_car_model=model, + id_car_serie=serie, + id_car_trim=trim, + year=year, + ) + context["url"] = url + context["cars"] = cars + return render(request, "recalls/recall_filter.html", context) + class RecallCreateView(FormView): - template_name = 'recalls/recall_create.html' + template_name = "recalls/recall_create.html" form_class = forms.RecallCreateForm - success_url = reverse_lazy('recall_success') + success_url = reverse_lazy("recall_success") def get_form(self, form_class=None): form = super().get_form(form_class) - make = self.request.GET.get('make') - model = self.request.GET.get('model') - serie = self.request.GET.get('serie') - trim = self.request.GET.get('trim') - year = self.request.GET.get('year') + make = self.request.GET.get("make") + model = self.request.GET.get("model") + serie = self.request.GET.get("serie") + trim = self.request.GET.get("trim") + year = self.request.GET.get("year") if make: qs = models.CarMake.objects.filter(pk=make) - form.fields['make'].queryset = qs - form.initial['make'] = qs.first() + form.fields["make"].queryset = qs + form.initial["make"] = qs.first() if model: qs = models.CarModel.objects.filter(pk=model) - form.fields['model'].queryset = qs - form.initial['model'] = qs.first() + form.fields["model"].queryset = qs + form.initial["model"] = qs.first() if serie: qs = models.CarSerie.objects.filter(pk=serie) - form.fields['serie'].queryset = qs - form.initial['serie'] = qs.first() + form.fields["serie"].queryset = qs + form.initial["serie"] = qs.first() if trim: qs = models.CarTrim.objects.filter(pk=trim) - form.fields['trim'].queryset = qs - form.initial['trim'] = qs.first() + form.fields["trim"].queryset = qs + form.initial["trim"] = qs.first() if year: - form.fields['year_from'].initial = year - form.fields['year_to'].initial = year + form.fields["year_from"].initial = year + form.fields["year_to"].initial = year return form def get_initial(self): initial = super().get_initial() - if self.request.method == 'GET': + if self.request.method == "GET": initial.update(self.request.GET.dict()) return initial @@ -11912,175 +12368,195 @@ class RecallCreateView(FormView): for dealer in dealers: dealer_cars = cars.filter(dealer=dealer) notification = models.RecallNotification.objects.create( - recall=recall, - dealer=dealer + recall=recall, dealer=dealer ) notification.cars_affected.set(dealer_cars) # Send email self.send_notification_email(dealer, recall, dealer_cars) - messages.success(self.request, _("Recall created and notifications sent successfully")) + messages.success( + self.request, _("Recall created and notifications sent successfully") + ) return super().form_valid(form) def send_notification_email(self, dealer, recall, cars): subject = f"Recall Notification: {recall.title}" - message = render_to_string('recalls/email/recall_notification.txt', { - 'dealer': dealer, - 'recall': recall, - 'cars': cars, - }) + message = render_to_string( + "recalls/email/recall_notification.txt", + { + "dealer": dealer, + "recall": recall, + "cars": cars, + }, + ) send_email( subject, message, - 'noreply@yourdomain.com', + "noreply@yourdomain.com", [dealer.user.email], ) + class RecallSuccessView(TemplateView): - template_name = 'recalls/recall_success.html' + template_name = "recalls/recall_success.html" @login_required -def schedule_calendar(request,dealer_slug): +def schedule_calendar(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at') - upcoming_schedules = user_schedules.filter(scheduled_at__gte=timezone.now()).order_by('scheduled_at') - context = { - 'schedules': user_schedules, - 'upcoming_schedules':upcoming_schedules - } - return render(request, 'schedule_calendar.html', context) + user_schedules = models.Schedule.objects.filter( + dealer=dealer, scheduled_by=request.user + ).order_by("scheduled_at") + upcoming_schedules = user_schedules.filter( + scheduled_at__gte=timezone.now() + ).order_by("scheduled_at") + context = {"schedules": user_schedules, "upcoming_schedules": upcoming_schedules} + return render(request, "schedule_calendar.html", context) # Support @login_required def help_center(request): - return render(request, 'support/help_center.html') + return render(request, "support/help_center.html") + @login_required -@permission_required('inventory.add_ticket') -def create_ticket(request,dealer_slug): +@permission_required("inventory.add_ticket") +def create_ticket(request, dealer_slug): if not request.is_dealer: - return redirect('home') + return redirect("home") dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - if request.method == 'POST': + if request.method == "POST": form = forms.TicketForm(request.POST) if form.is_valid(): instance = form.save(commit=False) instance.dealer = dealer instance.save() - messages.success(request, 'Your support ticket has been submitted successfully!') - return redirect('ticket_list',dealer_slug=dealer.slug) + messages.success( + request, "Your support ticket has been submitted successfully!" + ) + return redirect("ticket_list", dealer_slug=dealer.slug) else: form = forms.TicketForm() - return render(request, 'support/create_ticket.html', {'form': form}) + return render(request, "support/create_ticket.html", {"form": form}) + @login_required -@permission_required('inventory.view_ticket') -def ticket_list(request,dealer_slug): - dealer= get_object_or_404(models.Dealer, slug=dealer_slug) - tickets = models.Ticket.objects.filter(dealer=dealer).order_by('-created_at') - query=request.GET.get('q') - if query: - tickets=tickets.filter(Q(id__icontains=query)| Q(subject__icontains=query)) - - return render(request, 'support/ticket_list.html', {'tickets': tickets}) - -@login_required -@permission_required('inventory.change_ticket') -def ticket_detail(request, dealer_slug,ticket_id): +@permission_required("inventory.view_ticket") +def ticket_list(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - ticket = models.Ticket.objects.get(dealer=dealer,id=ticket_id) - return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + tickets = models.Ticket.objects.filter(dealer=dealer).order_by("-created_at") + query = request.GET.get("q") + if query: + tickets = tickets.filter(Q(id__icontains=query) | Q(subject__icontains=query)) + + return render(request, "support/ticket_list.html", {"tickets": tickets}) + @login_required -@permission_required('inventory.change_ticket') +@permission_required("inventory.change_ticket") +def ticket_detail(request, dealer_slug, ticket_id): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + ticket = models.Ticket.objects.get(dealer=dealer, id=ticket_id) + return render(request, "support/ticket_detail.html", {"ticket": ticket}) + + +@login_required +@permission_required("inventory.change_ticket") def ticket_mark_resolved(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) - ticket.status = 'resolved' + ticket.status = "resolved" ticket.save() - messages.success(request, 'Ticket marked as resolved successfully!') - subject = 'Ticket Resolved' + messages.success(request, "Ticket marked as resolved successfully!") + subject = "Ticket Resolved" message = f"Your support ticket has been resolved. Please check the details below:\n\nTicket ID: {ticket.id}\nSubject: {ticket.subject}\nDescription: {ticket.description}" - send_email( - settings.SUPPORT_EMAIL, - ticket.dealer.user.email, - subject, - message - ) - return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + send_email(settings.SUPPORT_EMAIL, ticket.dealer.user.email, subject, message) + return render(request, "support/ticket_detail.html", {"ticket": ticket}) + @login_required -@permission_required('inventory.change_ticket') +@permission_required("inventory.change_ticket") def ticket_update(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) - if request.method == 'POST': + if request.method == "POST": form = forms.TicketResolutionForm(request.POST, instance=ticket) if form.is_valid(): form.save() - messages.success(request, f'Ticket has been marked as {ticket.get_status_display()}.') - return redirect('ticket_detail',dealer_slug=ticket.dealer.slug, ticket_id=ticket.id) + messages.success( + request, f"Ticket has been marked as {ticket.get_status_display()}." + ) + return redirect( + "ticket_detail", dealer_slug=ticket.dealer.slug, ticket_id=ticket.id + ) else: form = forms.TicketResolutionForm(instance=ticket) - return render(request, 'support/ticket_update.html', { - 'ticket': ticket, - 'form': form - }) + return render( + request, "support/ticket_update.html", {"ticket": ticket, "form": form} + ) # class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): # template_name = 'chart_of_accounts/coa_list.html' # permission_required = 'django_ledger.view_chartofaccountmodel' class ChartOfAccountModelCreateView(ChartOfAccountModelCreateViewBase): - template_name = 'chart_of_accounts/coa_create.html' - permission_required = 'django_ledger.add_chartofaccountmodel' -class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): - template_name = 'chart_of_accounts/coa_list.html' - permission_required = 'django_ledger.view_chartofaccountmodel' -class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase): - template_name = 'chart_of_accounts/coa_update.html' - permission_required = 'django_ledger.change_chartofaccountmodel' -class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): - permission_required = 'django_ledger.change_chartofaccountmodel' + template_name = "chart_of_accounts/coa_create.html" + permission_required = "django_ledger.add_chartofaccountmodel" +class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): + template_name = "chart_of_accounts/coa_list.html" + permission_required = "django_ledger.view_chartofaccountmodel" + + +class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase): + template_name = "chart_of_accounts/coa_update.html" + permission_required = "django_ledger.change_chartofaccountmodel" + + +class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): + permission_required = "django_ledger.change_chartofaccountmodel" + class CarDealershipSignUpView(CreateView): model = models.UserRegistration form_class = forms.CarDealershipRegistrationForm - template_name = 'account/signup-wizard.html' - success_url = reverse_lazy('registration_success') + template_name = "account/signup-wizard.html" + success_url = reverse_lazy("registration_success") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['title'] = _('Car Dealership Registration') + context["title"] = _("Car Dealership Registration") return context def form_valid(self, form): response = super().form_valid(form) - messages.success(self.request, _('Your request has been submitted. We will contact you soon.')) + messages.success( + self.request, + _("Your request has been submitted. We will contact you soon."), + ) return response + def payment_result(request): s = request.GET.get("status") if s == "success": - return render(request, 'plans/payment_success.html') - return render(request, 'plans/payment_failed.html') + return render(request, "plans/payment_success.html") + return render(request, "plans/payment_failed.html") @require_POST -def create_estimate_for_car(request,dealer_slug,slug): +def create_estimate_for_car(request, dealer_slug, slug): car = get_object_or_404(models.Car, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - if request.method == 'POST': + if request.method == "POST": form = forms.CarDetailsEstimateCreate(request.POST) if form.is_valid(): - customer = form.cleaned_data['customer'] + customer = form.cleaned_data["customer"] estimate = create_estimate_(dealer, car, customer) if request.is_staff: @@ -12105,16 +12581,28 @@ def create_estimate_for_car(request,dealer_slug,slug): else: messages.error(request, "Please correct the errors below.") return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug) + + @require_POST -def estimate_create_from_opportunity(request,dealer_slug,slug): +def estimate_create_from_opportunity(request, dealer_slug, slug): opportunity = get_object_or_404(models.Opportunity, slug=slug) + if opportunity.estimate: + messages.error( + request, + "An estimate has already been created for this opportunity.", + ) + return redirect( + "opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug + ) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) car = opportunity.car customer = opportunity.customer - # TODO: set safe guard, so it doesnt recreate it - if not all([dealer,car,customer]): + + if not all([dealer, car, customer]): messages.error(request, "Please correct the errors below.") - return redirect("opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug) + return redirect( + "opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug + ) estimate = create_estimate_(dealer, car, customer) @@ -12136,4 +12624,4 @@ def estimate_create_from_opportunity(request,dealer_slug,slug): ) messages.success(request, "Estimate created successfully.") - return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) \ No newline at end of file + return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) diff --git a/requirements_prod.txt b/requirements_prod.txt index 7592a4f2..c236da77 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -7,7 +7,9 @@ autobahn==24.4.2 Automat==25.4.16 Babel==2.15.0 beautifulsoup4==4.13.4 +blacknoise==1.2.0 blessed==1.21.0 +Brotli==1.1.0 cattrs==25.1.1 certifi==2025.7.9 cffi==1.17.1 @@ -19,15 +21,13 @@ constantly==23.10.4 crispy-bootstrap5==2025.6 cryptography==45.0.5 cssbeautifier==1.15.4 -daphne==4.2.1 +cssselect2==0.8.0 defusedxml==0.7.1 diff-match-patch==20241021 distro==1.9.0 Django==5.2.4 django-allauth==65.10.0 django-appconf==1.1.0 -django-appointment==3.8.0 -django-background-tasks==1.2.8 django-bootstrap5==25.1 django-ckeditor==6.7.3 django-cors-headers==4.7.0 @@ -35,6 +35,7 @@ django-countries==7.6.1 django-crispy-forms==2.4 django-debug-toolbar==5.2.0 django-easy-audit==1.3.7 +django-encrypted-model-fields==0.6.5 django-extensions==4.1 django-filter==25.1 django-imagekit==5.0.0 @@ -47,7 +48,6 @@ django-ordered-model==3.7.4 django-phonenumber-field==8.0.0 django-picklefield==3.3 django-plans==2.0.0 -django-prometheus==2.4.1 django-q2==1.8.0 django-query-builder==3.2.0 django-schema-graph==3.1.0 @@ -56,8 +56,6 @@ django-tables2==2.7.5 django-treebeard==4.7.1 django-widget-tweaks==1.5.0 djangorestframework==3.16.0 -djhtml==3.0.8 -djlint==1.36.4 dnspython==2.7.0 docopt==0.6.2 EditorConfig==0.17.1 @@ -78,8 +76,6 @@ hyperlink==21.0.0 icalendar==6.3.1 idna==3.10 incremental==24.7.2 -iron-core==1.2.1 -iron-mq==0.9 jiter==0.10.0 jsbeautifier==1.15.4 json5==0.12.0 @@ -109,16 +105,17 @@ phonenumbers==8.13.42 pilkit==3.0 pillow==10.4.0 priority==1.3.0 -prometheus_client==0.22.1 psycopg2-binary==2.9.10 pyasn1==0.6.1 pyasn1_modules==0.4.2 pycparser==2.22 pydantic==2.11.7 pydantic_core==2.33.2 +pydyf==0.11.0 Pygments==2.19.2 pymongo==4.14.1 pyOpenSSL==25.1.0 +pyphen==0.17.2 python-dateutil==2.9.0.post0 python-dotenv==1.1.1 python-slugify==8.0.4 @@ -131,8 +128,6 @@ redis==6.2.0 regex==2024.11.6 requests==2.32.4 requests-toolbelt==1.0.0 -rich==14.0.0 -ruff==0.12.2 service-identity==24.2.0 setuptools==80.9.0 six==1.17.0 @@ -140,11 +135,15 @@ sniffio==1.3.1 soupsieve==2.7 SQLAlchemy==2.0.41 sqlparse==0.5.3 +starlette==0.47.3 +static3==0.7.0 suds==1.2.0 swapper==1.3.0 tablib==3.8.0 tenacity==9.1.2 text-unidecode==1.3 +tinycss2==1.4.0 +tinyhtml5==2.0.0 tqdm==4.67.1 Twisted==25.5.0 txaio==25.6.1 @@ -156,6 +155,8 @@ urllib3==2.5.0 uvicorn==0.35.0 uvicorn-worker==0.3.0 wcwidth==0.2.13 -whitenoise==6.9.0 +weasyprint==66.0 +webencodings==0.5.1 zope.interface==7.2 +zopfli==0.2.3.post1 zstandard==0.23.0 diff --git a/templates/account/account_inactive.html b/templates/account/account_inactive.html index fa1311ca..4ace5cad 100644 --- a/templates/account/account_inactive.html +++ b/templates/account/account_inactive.html @@ -6,9 +6,9 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Account Inactive" %} -{% endelement %} -{% element p %} -{% translate "This account is inactive." %} -{% endelement %} + {% translate "Account Inactive" %} + {% endelement %} + {% element p %} + {% translate "This account is inactive." %} + {% endelement %} {% endblock content %} diff --git a/templates/account/confirm_email_verification_code.html b/templates/account/confirm_email_verification_code.html index 08f0d893..84e37256 100644 --- a/templates/account/confirm_email_verification_code.html +++ b/templates/account/confirm_email_verification_code.html @@ -6,43 +6,43 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Enter Email Verification Code" %} -{% endelement %} -{% setvar email_link %} -{{ email }} -{% endsetvar %} -{% element p %} -{% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} -{% endelement %} -{% url 'account_email_verification_sent' as action_url %} -{% element form form=form method="post" action=action_url tags="entrance,email,verification" %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="prominent,confirm" %} -{% translate "Confirm" %} -{% endelement %} -{% if cancel_url %} - {% element button href=cancel_url tags="link,cancel" %} - {% translate "Cancel" %} -{% endelement %} -{% else %} -{% element button type="submit" form="logout-from-stage" tags="link,cancel" %} -{% translate "Cancel" %} -{% endelement %} -{% endif %} -{% endslot %} -{% endelement %} -{% if not cancel_url %} -
- - {% csrf_token %} -
-{% endif %} + {% translate "Enter Email Verification Code" %} + {% endelement %} + {% setvar email_link %} + {{ email }} + {% endsetvar %} + {% element p %} + {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} + {% endelement %} + {% url 'account_email_verification_sent' as action_url %} + {% element form form=form method="post" action=action_url tags="entrance,email,verification" %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="prominent,confirm" %} + {% translate "Confirm" %} + {% endelement %} + {% if cancel_url %} + {% element button href=cancel_url tags="link,cancel" %} + {% translate "Cancel" %} + {% endelement %} + {% else %} + {% element button type="submit" form="logout-from-stage" tags="link,cancel" %} + {% translate "Cancel" %} + {% endelement %} + {% endif %} + {% endslot %} + {% endelement %} + {% if not cancel_url %} +
+ + {% csrf_token %} +
+ {% endif %} {% endblock content %} diff --git a/templates/account/confirm_login_code..html b/templates/account/confirm_login_code..html index a7ffaaa1..ee6519c3 100644 --- a/templates/account/confirm_login_code..html +++ b/templates/account/confirm_login_code..html @@ -28,30 +28,30 @@

{% translate "Enter Sign-In Code" %}

{% setvar email_link %} - {{ email }} - {% endsetvar %} -

- {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} -

-
- {% csrf_token %} - {{ redirect_field }} - {{ form|crispy }} - -
- {% element button type="submit" form="logout-from-stage" tags="link" %} - {% translate "Cancel" %} - {% endelement %} -
- - {% csrf_token %} -
+ {{ email }} + {% endsetvar %} +

+ {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} +

+
+ {% csrf_token %} + {{ redirect_field }} + {{ form|crispy }} + +
+ {% element button type="submit" form="logout-from-stage" tags="link" %} + {% translate "Cancel" %} + {% endelement %} +
+ + {% csrf_token %} +
+ + - - {% endblock content %} diff --git a/templates/account/email.html b/templates/account/email.html index 8faf8174..1a1fba70 100644 --- a/templates/account/email.html +++ b/templates/account/email.html @@ -50,7 +50,7 @@ {% if emailaddress.primary %} {% endif %} - {% endwith %} + {% endwith %} {% endfor %} diff --git a/templates/account/email_change.html b/templates/account/email_change.html index 94576c23..0ee140c6 100644 --- a/templates/account/email_change.html +++ b/templates/account/email_change.html @@ -6,63 +6,63 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% trans "Email Address" %} -{% endelement %} -{% if not emailaddresses %} - {% include "account/snippets/warn_no_email.html" %} -{% endif %} -{% url 'account_email' as action_url %} -{% element form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{% if current_emailaddress %} - {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} - {% slot label %} - {% translate "Current email" %}: -{% endslot %} -{% endelement %} -{% endif %} -{% if new_emailaddress %} - {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} - {% slot label %} - {% if not current_emailaddress %} - {% translate "Current email" %}: - {% else %} - {% translate "Changing to" %}: + {% trans "Email Address" %} + {% endelement %} + {% if not emailaddresses %} + {% include "account/snippets/warn_no_email.html" %} + {% endif %} + {% url 'account_email' as action_url %} + {% element form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {% if current_emailaddress %} + {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} + {% slot label %} + {% translate "Current email" %}: + {% endslot %} + {% endelement %} + {% endif %} + {% if new_emailaddress %} + {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} + {% slot label %} + {% if not current_emailaddress %} + {% translate "Current email" %}: + {% else %} + {% translate "Changing to" %}: + {% endif %} + {% endslot %} + {% slot help_text %} + {% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} + {% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} + {% trans 'Re-send Verification' %} + {% endelement %} + {% if current_emailaddress %} + {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} + {% trans 'Cancel Change' %} + {% endelement %} + {% endif %} + {% endslot %} + {% endelement %} + {% endif %} + {% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} + {% slot label %} + {% translate "Change to" %}: + {% endslot %} + {% endelement %} + {% endslot %} + {% slot actions %} + {% element button name="action_add" type="submit" %} + {% trans "Change Email" %} + {% endelement %} + {% endslot %} + {% endelement %} + {% if new_emailaddress %} + {% endif %} -{% endslot %} -{% slot help_text %} -{% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} -{% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} -{% trans 'Re-send Verification' %} -{% endelement %} -{% if current_emailaddress %} - {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} - {% trans 'Cancel Change' %} -{% endelement %} -{% endif %} -{% endslot %} -{% endelement %} -{% endif %} -{% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} -{% slot label %} -{% translate "Change to" %}: -{% endslot %} -{% endelement %} -{% endslot %} -{% slot actions %} -{% element button name="action_add" type="submit" %} -{% trans "Change Email" %} -{% endelement %} -{% endslot %} -{% endelement %} -{% if new_emailaddress %} - -{% endif %} {% endblock content %} diff --git a/templates/account/lock-screen.html b/templates/account/lock-screen.html index 5447cb7a..5d04e1e5 100644 --- a/templates/account/lock-screen.html +++ b/templates/account/lock-screen.html @@ -153,437 +153,437 @@

I need help with something

-

I can’t reorder a product I previously ordered

- -
-

How do I place an order?

- -
-

My payment method not working

- -
- -
-
- + href="#!"> +

I can’t reorder a product I previously ordered

+ + +

How do I place an order?

+ +
+

My payment method not working

+ +
+
+
+
+ +
+
Eric
+

+ Ask us anything – we’ll get back to you here or by email within 24 hours. +

+
+
+ + -
Eric
-

- Ask us anything – we’ll get back to you here or by email within 24 hours. -

- - - - - - - + -
-
-
-
-
- Theme Customizer -
-

Explore different styles according to your preferences

+
+
+
+
+
+ Theme Customizer +
+

Explore different styles according to your preferences

+
+ +
+
- -
- -
-
-
-
Color Scheme
-
-
- - +
+
+
Color Scheme
+
+
+ + +
+
+ + +
+
+ + +
+
-
- - +
+
+
RTL
+
+ +
+
+

Change text direction

-
- - +
+
+
Support Chat
+
+ +
+
+

Toggle support chat

+
+
Navigation Type
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ You can't update navigation type in this page +

+
+
+
Vertical Navbar Appearance
+
+
+ + +
+
+ + +
+
+

+ You can't update vertical navbar appearance in this page +

+
+
+
Horizontal Navbar Shape
+
+
+ + +
+
+ + +
+
+

+ You can't update horizontal navbar shape in this page +

+
+
+
Horizontal Navbar Appearance
+
+
+ + +
+
+ + +
+
+

+ You can't update horizontal navbar appearance in this page +

+
+ Purchase template
- - -
-
-
- - - - - - -
-
- customize -
-
+ - - - - - - - - - - - + + + + + + + + + + + diff --git a/templates/account/login.html b/templates/account/login.html index e2cb0db6..09d91e06 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -77,25 +77,25 @@
- + {% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
{% element button_group vertical=True %} - {% if PASSKEY_LOGIN_ENABLED %} - {% element button type="submit" form="mfa_login" id="passkey_login" tags="prominent,login,outline,primary" %} - {% trans "Sign in with a passkey" %} + {% if PASSKEY_LOGIN_ENABLED %} + {% element button type="submit" form="mfa_login" id="passkey_login" tags="prominent,login,outline,primary" %} + {% trans "Sign in with a passkey" %} + {% endelement %} + {% endif %} + {% if LOGIN_BY_CODE_ENABLED %} + {% element button href=request_login_code_url tags="prominent,login,outline,primary" %} + {% trans "Mail me a sign-in code" %} + {% endelement %} + {% endif %} {% endelement %} {% endif %} - {% if LOGIN_BY_CODE_ENABLED %} - {% element button href=request_login_code_url tags="prominent,login,outline,primary" %} - {% trans "Mail me a sign-in code" %} - {% endelement %} - {% endif %} -{% endelement %} -{% endif %} -{% if SOCIALACCOUNT_ENABLED %} - {% include "socialaccount/snippets/login.html" with page_layout="entrance" %} -{% endif %} + {% if SOCIALACCOUNT_ENABLED %} + {% include "socialaccount/snippets/login.html" with page_layout="entrance" %} + {% endif %} {% endblock content %} {% block extra_body %} {{ block.super }} diff --git a/templates/account/password_change.html b/templates/account/password_change.html index 36546bb3..4d6e51e5 100644 --- a/templates/account/password_change.html +++ b/templates/account/password_change.html @@ -27,7 +27,7 @@

{% trans "Change Password" %}

diff --git a/templates/account/password_set.html b/templates/account/password_set.html index e2325905..2cb77a42 100644 --- a/templates/account/password_set.html +++ b/templates/account/password_set.html @@ -6,20 +6,20 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% trans "Set Password" %} -{% endelement %} -{% url 'account_set_password' as action_url %} -{% element form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{{ redirect_field }} -{% element fields form=form %} -{% endelement %} -{% endslot %} -{% slot actions %} -{% element button type="submit" name="action" %} -{% trans 'Set Password' %} -{% endelement %} -{% endslot %} -{% endelement %} + {% trans "Set Password" %} + {% endelement %} + {% url 'account_set_password' as action_url %} + {% element form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {{ redirect_field }} + {% element fields form=form %} + {% endelement %} + {% endslot %} + {% slot actions %} + {% element button type="submit" name="action" %} + {% trans 'Set Password' %} + {% endelement %} + {% endslot %} + {% endelement %} {% endblock content %} diff --git a/templates/account/reauthenticate.html b/templates/account/reauthenticate.html index 5e7b3e58..d98c86a5 100644 --- a/templates/account/reauthenticate.html +++ b/templates/account/reauthenticate.html @@ -3,20 +3,20 @@ {% load i18n %} {% block reauthenticate_content %} {% element p %} - {% blocktranslate %}Enter your password:{% endblocktranslate %} -{% endelement %} -{% url 'account_reauthenticate' as action_url %} -{% element form form=form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="primary,reauthenticate" %} -{% trans "Confirm" %} -{% endelement %} -{% endslot %} -{% endelement %} + {% blocktranslate %}Enter your password:{% endblocktranslate %} + {% endelement %} + {% url 'account_reauthenticate' as action_url %} + {% element form form=form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="primary,reauthenticate" %} + {% trans "Confirm" %} + {% endelement %} + {% endslot %} + {% endelement %} {% endblock %} diff --git a/templates/account/request_login_code.html b/templates/account/request_login_code.html index 6d331faf..8b9b0152 100644 --- a/templates/account/request_login_code.html +++ b/templates/account/request_login_code.html @@ -6,27 +6,27 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Mail me a sign-in code" %} -{% endelement %} -{% element p %} -{% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %} -{% endelement %} -{% url 'account_request_login_code' as login_url %} -{% element form form=form method="post" action=login_url tags="entrance,login" %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="prominent,login" %} -{% translate "Request Code" %} -{% endelement %} -{% endslot %} -{% endelement %} -{% url 'account_login' as login_url %} -{% element button href=login_url tags="link" %} -{% translate "Other sign-in options" %} -{% endelement %} + {% translate "Mail me a sign-in code" %} + {% endelement %} + {% element p %} + {% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %} + {% endelement %} + {% url 'account_request_login_code' as login_url %} + {% element form form=form method="post" action=login_url tags="entrance,login" %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="prominent,login" %} + {% translate "Request Code" %} + {% endelement %} + {% endslot %} + {% endelement %} + {% url 'account_login' as login_url %} + {% element button href=login_url tags="link" %} + {% translate "Other sign-in options" %} + {% endelement %} {% endblock content %} diff --git a/templates/account/signup-wizard.html b/templates/account/signup-wizard.html index 3dc3d472..53e2af1f 100644 --- a/templates/account/signup-wizard.html +++ b/templates/account/signup-wizard.html @@ -25,7 +25,7 @@

{% trans 'Create your dealership account today' %}

-
+
{% csrf_token %}
@@ -55,16 +55,16 @@ - +
- - + + {% endblock content %} - + {% block customJS %} - + - - {% endblock %} +{% endblock %} +{% block customJS %} + +{% endblock %} diff --git a/templates/bill/bill_detail.html b/templates/bill/bill_detail.html index c68457ef..dcd01e13 100644 --- a/templates/bill/bill_detail.html +++ b/templates/bill/bill_detail.html @@ -94,6 +94,7 @@ {% trans 'PO' %} + {% for bill_item in itemtxs_qs %} @@ -161,4 +162,4 @@
{% include "bill/includes/mark_as.html" %} - {% endblock %} +{% endblock %} diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index 4873cba1..4c4bc747 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -50,13 +50,13 @@
{% modal_action bill 'get' entity_slug %} -
+
{% trans 'View' %} {% if perms.django_ledger.change_billmodel %} {% trans 'Update' %} + href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}" + class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %} {% if bill.can_pay %} @@ -199,12 +199,12 @@ {% endif %}
+ +
diff --git a/templates/inventory/add_colors.html b/templates/inventory/add_colors.html index 0cf8fda8..d66e7fa9 100644 --- a/templates/inventory/add_colors.html +++ b/templates/inventory/add_colors.html @@ -2,68 +2,68 @@ {% load i18n %} {% block title %} {% trans 'Add Colors' %} {% endblock %} - {% block content %} -
-

{% trans "Add Colors" %}

-

- {% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }} -

-
- {% csrf_token %} +{% block content %} +
+

{% trans "Add Colors" %}

+

+ {% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }} +

+ + {% csrf_token %} -
-

{% trans 'Exterior Colors' %}

- {% for color in form.fields.exterior.queryset %} -
-
-