From 4d63b17e6834bc6a623972d0dbf0d844f1dee447 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 27 Aug 2025 12:59:19 +0300 Subject: [PATCH 1/2] merge complete --- .../plans/billing_info_create_or_update.html | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/templates/plans/billing_info_create_or_update.html b/templates/plans/billing_info_create_or_update.html index 64e6ca47..d43000a5 100644 --- a/templates/plans/billing_info_create_or_update.html +++ b/templates/plans/billing_info_create_or_update.html @@ -1,6 +1,38 @@ {% extends 'base.html' %} {% load i18n crispy_forms_filters %} {% block title %} + {% trans 'Billing Information' %}{% endblock %} + {% block content %} + {% comment %}
+
+
+ + +

{% trans "Provide billing data"|upper %}

+ {% csrf_token %} + {{ form|crispy }} + + + {% if object %} + {{ _("Delete") }} + {% endif %} + +
+
+
{% endcomment %} + + + +
+ +
+
+
+

+ {% trans "Provide billing data"|upper %} +

{% trans 'Billing Information' %} {% endblock %} {% block content %} @@ -24,12 +56,7 @@ {{ _("Save") }} - {% if object %} - - - {{ _("Delete") }} - - {% endif %} + {{ _("Cancel") }}
From 2bbcae2e7a79d6b6fb16d6c8f67e1c2cfc843561 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 27 Aug 2025 13:04:41 +0300 Subject: [PATCH 2/2] lint and formate --- car_inventory/asgi.py | 24 +- inventory/forms.py | 119 +- inventory/hooks.py | 10 +- .../commands/invoices_due_date_reminder.py | 73 +- inventory/management/commands/led.py | 50 +- inventory/management/commands/p.py | 7 +- .../management/commands/plans_maintenance.py | 22 +- inventory/management/commands/run1.py | 7 +- inventory/management/commands/seed.py | 31 +- inventory/management/commands/seed1.py | 105 +- inventory/middleware.py | 21 +- inventory/models.py | 316 ++-- inventory/notifications/sse.py | 87 +- inventory/override.py | 379 ++-- inventory/signals.py | 93 +- inventory/tasks.py | 148 +- inventory/templatetags/custom_filters.py | 14 +- inventory/urls.py | 167 +- inventory/utils.py | 470 ++--- inventory/validators.py | 6 +- inventory/views.py | 1622 ++++++++++------- load_json_data.py | 13 +- scripts/set_plans.py | 10 +- templates/403.html | 334 ++-- templates/404.html | 42 +- templates/500.html | 42 +- templates/account/lock-screen.html | 46 +- templates/account/login.html | 5 +- templates/account/signup-wizard.html | 182 +- templates/account/user_settings.html | 70 +- .../confirm_activate_account.html | 2 +- templates/admin_management/management.html | 52 +- .../email_change_verification_code.html | 10 +- templates/administration/manage_day_off.html | 10 +- .../administration/manage_staff_member.html | 10 +- .../manage_staff_personal_info.html | 10 +- .../administration/manage_working_hours.html | 29 +- templates/administration/staff_index.html | 10 +- templates/appointment/appointments.html | 14 +- templates/appointment/default_thank_you.html | 10 +- .../appointment/enter_verification_code.html | 3 +- .../appointment/rescheduling_thank_you.html | 10 +- templates/appointment/thank_you.html | 10 +- templates/auth_base.html | 6 +- templates/base.html | 143 +- templates/bill/bill_create.html | 80 +- templates/bill/bill_detail.html | 318 ++-- templates/bill/bill_update.html | 3 +- templates/bill/includes/card_bill.html | 81 +- templates/bill/tags/bill_item_formset.html | 253 +-- templates/chart_of_accounts/coa_create.html | 98 +- templates/chart_of_accounts/coa_list.html | 17 +- templates/chart_of_accounts/coa_update.html | 27 +- .../chart_of_accounts/includes/coa_card.html | 44 +- templates/components/email_modal.html | 30 +- templates/components/note_modal.html | 10 +- templates/components/schedule_modal.html | 8 +- templates/components/task_modal.html | 23 +- templates/crm/leads/lead_detail.html | 181 +- templates/crm/leads/lead_form.html | 71 +- templates/crm/leads/lead_list.html | 477 +++-- templates/crm/leads/lead_tracking.html | 311 ++-- templates/crm/leads/lead_view.html | 34 +- .../crm/leads/partials/update_action.html | 13 +- templates/crm/notifications_history.html | 4 +- .../crm/opportunities/opportunity_detail.html | 149 +- .../crm/opportunities/opportunity_form.html | 341 ++-- .../opportunities/opportunity_list copy.html | 3 +- .../crm/opportunities/opportunity_list.html | 215 +-- .../partials/opportunity_grid.html | 30 +- templates/csv_upload.html | 8 +- templates/customers/customer_form.html | 92 +- templates/customers/customer_list.html | 331 ++-- templates/customers/view_customer.html | 156 +- .../dashboards/aging_inventory_list.html | 250 +-- templates/dashboards/chart.html | 221 +-- .../dashboards/financial_data_cards.html | 578 +++--- templates/dashboards/general_dashboard.html | 856 +++++---- templates/dashboards/manager_dashboard.html | 640 +++---- templates/dashboards/sales_dashboard.html | 544 +++--- templates/dealers/activity_log.html | 161 +- templates/dealers/assign_car_makes.html | 74 +- templates/dealers/dealer_detail.html | 477 +++-- templates/dealers/dealer_form.html | 62 +- templates/email_sender/thank_you_email.html | 5 +- templates/emails/expiration_reminder_ar.html | 32 +- templates/emails/expiration_reminder_en.html | 28 +- templates/emails/schedule_reminder.html | 70 +- templates/empty-illustration-page.html | 132 +- templates/errors/404.html | 46 +- templates/errors/500.html | 46 +- templates/footer.html | 119 +- templates/groups/group_form.html | 88 +- templates/groups/group_list.html | 133 +- templates/groups/group_permission_form.html | 256 ++- templates/haikalbot/chatbot.html | 12 +- templates/header.html | 1190 ++++++------ templates/index.html | 8 +- templates/inventory/add_colors.html | 120 +- templates/inventory/car_confirm_delete.html | 61 +- templates/inventory/car_detail.html | 234 ++- templates/inventory/car_edit.html | 58 +- templates/inventory/car_finance_form.html | 90 +- templates/inventory/car_form.html | 1416 +++++++------- templates/inventory/car_list.html | 2 +- templates/inventory/car_list_view.html | 65 +- templates/inventory/inventory_stats.html | 280 ++- templates/items/expenses/expense_create.html | 59 +- templates/items/expenses/expense_update.html | 58 +- templates/items/expenses/expenses_list.html | 139 +- templates/items/service/service_create.html | 90 +- templates/items/service/service_list.html | 153 +- .../bank_accounts/bank_account_detail.html | 40 +- .../bank_accounts/bank_account_form.html | 90 +- .../bank_accounts/bank_account_list.html | 145 +- templates/ledger/bills/bill_detail.html | 47 +- templates/ledger/bills/bill_form.html | 4 +- templates/ledger/bills/bill_list.html | 161 +- .../ledger/coa_accounts/account_form.html | 69 +- .../ledger/coa_accounts/account_list.html | 378 ++-- .../coa_accounts/partials/account_table.html | 4 +- .../journal_entry/journal_entry_form.html | 60 +- .../journal_entry/journal_entry_list.html | 253 ++- .../journal_entry_transactions.html | 2 +- templates/ledger/ledger/ledger_delete.html | 3 +- templates/ledger/ledger/ledger_form.html | 60 +- templates/ledger/ledger/ledger_list.html | 248 +-- templates/ledger/reports/balance_sheet.html | 89 +- templates/ledger/reports/car_sale_report.html | 576 +++--- .../ledger/reports/cash_flow_statement.html | 75 +- .../ledger/reports/income_statement.html | 79 +- templates/ledger/reports/purchase_report.html | 263 ++- templates/message-illustration.html | 112 +- templates/modal/delete_modal.html | 49 +- templates/notifications.html | 13 +- .../organizations/organization_detail.html | 121 +- .../organizations/organization_form.html | 94 +- .../organizations/organization_list.html | 353 ++-- templates/partials/pagination.html | 56 +- templates/partials/search_box.html | 9 +- templates/partials/tables.html | 14 +- templates/partials/task.html | 3 +- templates/payment_success.html | 60 +- .../plans/billing_info_create_or_update.html | 86 +- templates/plans/extend.html | 16 +- templates/plans/invoices/layout.html | 34 +- templates/plans/order_detail.html | 121 +- templates/plans/order_list.html | 113 +- templates/plans/plan_table.html | 19 +- templates/pricing_page.html | 889 ++++----- .../car_inventory_item_form.html | 112 +- .../purchase_orders/includes/card_po.html | 30 +- .../purchase_orders/partials/po-select.html | 4 +- templates/purchase_orders/po_form.html | 86 +- templates/purchase_orders/po_list.html | 247 ++- .../purchase_orders/tags/po_item_table.html | 8 +- .../recalls/partials/recall_cars_table.html | 22 +- .../recalls/partials/recall_filter_form.html | 41 +- templates/recalls/recall_create.html | 118 +- templates/recalls/recall_detail.html | 134 +- templates/recalls/recall_filter.html | 66 +- templates/recalls/recall_list.html | 167 +- templates/recalls/recall_success.html | 19 +- .../sales/estimates/estimate_detail.html | 250 +-- .../sales/estimates/estimate_form-copy.html | 5 +- templates/sales/estimates/estimate_form.html | 448 ++--- templates/sales/estimates/estimate_list.html | 142 +- .../sales/estimates/estimate_preview.html | 19 +- .../sales/estimates/sale_order_form.html | 60 +- templates/sales/invoices/invoice_create.html | 58 +- templates/sales/invoices/invoice_detail.html | 153 +- templates/sales/invoices/invoice_list.html | 160 +- templates/sales/invoices/invoice_preview.html | 27 +- templates/sales/invoices/invoice_update.html | 20 +- templates/sales/orders/order_details.html | 22 +- templates/sales/orders/purchase_order.html | 70 +- templates/sales/payments/payment_form1.html | 3 +- templates/sales/payments/payment_list.html | 2 +- templates/sales/saleorder_detail.html | 64 +- templates/sales/sales_list.html | 235 +-- .../sales/tags/invoice_item_formset.html | 29 +- templates/schedule_calendar.html | 403 ++-- templates/staff/staff_detail.html | 244 ++- templates/support/create_ticket.html | 58 +- templates/support/help_center.html | 6 +- templates/support/ticket_detail.html | 117 +- templates/support/ticket_list.html | 200 +- templates/support/ticket_update.html | 26 +- templates/users/user_detail.html | 207 +-- templates/users/user_form.html | 92 +- templates/users/user_group_form.html | 85 +- templates/users/user_list.html | 32 +- templates/users/user_password_reset.html | 67 +- templates/vendors/vendor_form.html | 95 +- templates/vendors/vendors_list.html | 306 ++-- templates/vendors/view_vendor.html | 216 +-- templates/welcome-temp.html | 6 +- templates/welcome.html | 15 +- templates/welcome_base.html | 37 +- 199 files changed, 13794 insertions(+), 13152 deletions(-) diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 6eba063c..78e43762 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -10,9 +10,11 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ # asgi.py import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") import django + django.setup() @@ -30,11 +32,17 @@ from django.core.asgi import get_asgi_application # # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), # } # ) -application = ProtocolTypeRouter({ - "http": AuthMiddlewareStack( - URLRouter([ - path("sse/notifications/", NotificationSSEApp()), - re_path(r"", get_asgi_application()), # All other routes go to Django - ]) - ), -}) \ No newline at end of file +application = ProtocolTypeRouter( + { + "http": AuthMiddlewareStack( + URLRouter( + [ + path("sse/notifications/", NotificationSSEApp()), + re_path( + r"", get_asgi_application() + ), # All other routes go to Django + ] + ) + ), + } +) diff --git a/inventory/forms.py b/inventory/forms.py index 1a49882a..bfc563b7 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -56,7 +56,7 @@ from .models import ( DealerSettings, Tasks, Recall, - Ticket + Ticket, ) from django_ledger import models as ledger_models from django.forms import ( @@ -146,9 +146,16 @@ class StaffForm(forms.ModelForm): ) class Meta: - model = Staff - fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"] + fields = [ + "first_name", + "last_name", + "arabic_name", + "phone_number", + "address", + "logo", + "group", + ] # Dealer Form @@ -439,13 +446,15 @@ class CarFinanceForm(forms.ModelForm): marked_price = cleaned_data.get("marked_price") if cost_price > marked_price: - raise forms.ValidationError({"cost_price": "Cost price should not be greater than marked price"}) + raise forms.ValidationError( + {"cost_price": "Cost price should not be greater than marked price"} + ) return cleaned_data class Meta: model = Car - fields = ["cost_price","marked_price"] + fields = ["cost_price", "marked_price"] class CarLocationForm(forms.ModelForm): @@ -1168,7 +1177,7 @@ class ScheduleForm(forms.ModelForm): scheduled_at = forms.DateTimeField( widget=DateTimeInput(attrs={"type": "datetime-local"}) ) - reminder = forms.BooleanField(help_text=_("Send a reminder?"),required=False) + reminder = forms.BooleanField(help_text=_("Send a reminder?"), required=False) class Meta: model = Schedule @@ -1289,6 +1298,7 @@ class OpportunityForm(forms.ModelForm): if self.instance and self.instance.pk: self.fields["probability"].initial = self.instance.probability + class OpportunityStageForm(forms.ModelForm): """ Represents a form for creating or editing Opportunity instances. @@ -1305,17 +1315,13 @@ class OpportunityStageForm(forms.ModelForm): :type Meta.fields: list """ - class Meta: model = Opportunity fields = [ "stage", - ] - - class InvoiceModelCreateForm(InvoiceModelCreateFormBase): """ Represents a form for creating an Invoice model that inherits from a base @@ -1633,8 +1639,7 @@ class PermissionForm(forms.ModelForm): "django_ledger.billmodeldjango_ledger.itemmodel", "django_ledger.invoicemodel", "django_ledger.vendormodel", - "django_ledger.journalentrymodel" - "django_ledger.purchaseordermodel", + "django_ledger.journalentrymodeldjango_ledger.purchaseordermodel", ] permissions = cache.get( @@ -2138,91 +2143,115 @@ class VatRateForm(forms.ModelForm): class CustomSetPasswordForm(SetPasswordForm): new_password1 = forms.CharField( label="New Password", - widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'}) + widget=forms.PasswordInput( + attrs={"class": "form-control", "placeholder": "New Password"} + ), ) new_password2 = forms.CharField( label="Confirm New Password", - widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'}) + widget=forms.PasswordInput( + attrs={"class": "form-control", "placeholder": "Confirm New Password"} + ), ) + # forms.py class RecallFilterForm(forms.Form): make = forms.ModelChoiceField( queryset=CarMake.objects.all(), required=False, label=_("Make"), - widget=forms.Select(attrs={'class': 'form-control'}) + widget=forms.Select(attrs={"class": "form-control"}), ) model = forms.ModelChoiceField( queryset=CarModel.objects.none(), required=False, label=_("Model"), - widget=forms.Select(attrs={'class': 'form-control'}) + widget=forms.Select(attrs={"class": "form-control"}), ) serie = forms.ModelChoiceField( queryset=CarSerie.objects.none(), required=False, label=_("Series"), - widget=forms.Select(attrs={'class': 'form-control'}) + widget=forms.Select(attrs={"class": "form-control"}), ) trim = forms.ModelChoiceField( queryset=CarTrim.objects.none(), required=False, label=_("Trim"), - widget=forms.Select(attrs={'class': 'form-control'}) + widget=forms.Select(attrs={"class": "form-control"}), + ) + year_from = forms.IntegerField( + required=False, + label=_("From Year"), + widget=forms.NumberInput(attrs={"class": "form-control"}), + ) + year_to = forms.IntegerField( + required=False, + label=_("To Year"), + widget=forms.NumberInput(attrs={"class": "form-control"}), ) - year_from = forms.IntegerField(required=False, label=_("From Year"), - widget=forms.NumberInput(attrs={'class': 'form-control'})) - year_to = forms.IntegerField(required=False, label=_("To Year"), - widget=forms.NumberInput(attrs={'class': 'form-control'})) def __init__(self, *args, **kwargs): - make_id = kwargs.pop('make_id', None) - model_id = kwargs.pop('model_id', None) - serie_id = kwargs.pop('serie_id', None) + make_id = kwargs.pop("make_id", None) + model_id = kwargs.pop("model_id", None) + serie_id = kwargs.pop("serie_id", None) super().__init__(*args, **kwargs) if make_id: - self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id) + self.fields["model"].queryset = CarModel.objects.filter( + id_car_make_id=make_id + ) if model_id: - self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id) + self.fields["serie"].queryset = CarSerie.objects.filter( + id_car_model_id=model_id + ) if serie_id: - self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id) + self.fields["trim"].queryset = CarTrim.objects.filter( + id_car_serie_id=serie_id + ) + + class RecallCreateForm(forms.ModelForm): class Meta: model = Recall - fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to'] + fields = [ + "title", + "description", + "make", + "model", + "serie", + "trim", + "year_from", + "year_to", + ] widgets = { - 'make': forms.Select(attrs={'class': 'form-control'}), - 'model': forms.Select(attrs={'class': 'form-control'}), - 'serie': forms.Select(attrs={'class': 'form-control'}), - 'trim': forms.Select(attrs={'class': 'form-control'}), - 'title': forms.TextInput(attrs={'class': 'form-control'}), - 'description': forms.Textarea(attrs={'class': 'form-control'}), - 'year_from': forms.NumberInput(attrs={'class': 'form-control'}), - 'year_to': forms.NumberInput(attrs={'class': 'form-control'}), + "make": forms.Select(attrs={"class": "form-control"}), + "model": forms.Select(attrs={"class": "form-control"}), + "serie": forms.Select(attrs={"class": "form-control"}), + "trim": forms.Select(attrs={"class": "form-control"}), + "title": forms.TextInput(attrs={"class": "form-control"}), + "description": forms.Textarea(attrs={"class": "form-control"}), + "year_from": forms.NumberInput(attrs={"class": "form-control"}), + "year_to": forms.NumberInput(attrs={"class": "form-control"}), } class TicketForm(forms.ModelForm): class Meta: model = Ticket - fields = ['subject', 'description', 'priority'] + fields = ["subject", "description", "priority"] widgets = { - 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}), + "description": forms.Textarea(attrs={"class": "form-control", "rows": 10}), } class TicketResolutionForm(forms.ModelForm): - class Meta: model = Ticket - fields = ['status', 'resolution_notes'] + fields = ["status", "resolution_notes"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit status choices to resolution options - self.fields['status'].choices = [ - ('resolved', 'Resolved'), - ('closed', 'Closed') - ] \ No newline at end of file + self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")] diff --git a/inventory/hooks.py b/inventory/hooks.py index 548df7e1..36a793b1 100644 --- a/inventory/hooks.py +++ b/inventory/hooks.py @@ -1,9 +1,10 @@ import logging from inventory.models import Dealer -from .utils import get_accounts_data,create_account +from .utils import get_accounts_data, create_account logger = logging.getLogger(__name__) + def check_create_coa_accounts(task): logger.info("Checking if all accounts are created") instance = task.kwargs["dealer"] @@ -17,7 +18,8 @@ def check_create_coa_accounts(task): logger.info(f"Default account does not exist: {account_data['code']}") create_account(entity, coa, account_data) + def print_results(task): - dealer= task.kwargs["dealer"] - print("HOOK: ",dealer) - print("HOOK: ",dealer.pk) \ No newline at end of file + dealer = task.kwargs["dealer"] + print("HOOK: ", dealer) + print("HOOK: ", dealer.pk) diff --git a/inventory/management/commands/invoices_due_date_reminder.py b/inventory/management/commands/invoices_due_date_reminder.py index d331ed6c..84ccd8bd 100644 --- a/inventory/management/commands/invoices_due_date_reminder.py +++ b/inventory/management/commands/invoices_due_date_reminder.py @@ -8,11 +8,12 @@ from django.core.management.base import BaseCommand from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType -from django_ledger.models import InvoiceModel,EstimateModel -from inventory.models import ExtraInfo,Notification,CustomGroup +from django_ledger.models import InvoiceModel, EstimateModel +from inventory.models import ExtraInfo, Notification, CustomGroup logger = logging.getLogger(__name__) + class Command(BaseCommand): help = "Handles invoices due date reminders" @@ -33,27 +34,30 @@ class Command(BaseCommand): def invocie_expiration_reminders(self): """Queue email reminders for expiring plans""" - reminder_days = getattr(settings, 'INVOICE_PAST_DUE_REMIND', [3, 7, 14]) + reminder_days = getattr(settings, "INVOICE_PAST_DUE_REMIND", [3, 7, 14]) today = timezone.now().date() for days in reminder_days: target_date = today + timedelta(days=days) expiring_plans = InvoiceModel.objects.filter( date_due=target_date - ).select_related('customer','ce_model') + ).select_related("customer", "ce_model") for inv in expiring_plans: # dealer = inv.customer.customer_set.first().dealer subject = f"Your invoice is due in {days} days" - message = render_to_string('emails/invoice_past_due_reminder.txt', { - 'customer_name': inv.customer.customer_name, - 'invoice_number': inv.invoice_number, - 'amount_due': inv.amount_due, - 'days_past_due': inv.due_in_days(), - 'SITE_NAME': settings.SITE_NAME - }) + message = render_to_string( + "emails/invoice_past_due_reminder.txt", + { + "customer_name": inv.customer.customer_name, + "invoice_number": inv.invoice_number, + "amount_due": inv.amount_due, + "days_past_due": inv.due_in_days(), + "SITE_NAME": settings.SITE_NAME, + }, + ) send_email( - 'noreply@yourdomain.com', + "noreply@yourdomain.com", inv.customer.email, subject, message, @@ -65,21 +69,24 @@ class Command(BaseCommand): """Queue email reminders for expiring plans""" today = timezone.now().date() expiring_plans = InvoiceModel.objects.filter( - date_due__lte = today - ).select_related('customer','ce_model') + date_due__lte=today + ).select_related("customer", "ce_model") # Send email for inv in expiring_plans: dealer = inv.customer.customer_set.first().dealer subject = f"Your invoice is past due" - message = render_to_string('emails/invoice_past_due.txt', { - 'customer_name': inv.customer.customer_name, - 'invoice_number': inv.invoice_number, - 'amount_due': inv.amount_due, - 'days_past_due': (today - inv.date_due).days, - 'SITE_NAME': settings.SITE_NAME - }) + message = render_to_string( + "emails/invoice_past_due.txt", + { + "customer_name": inv.customer.customer_name, + "invoice_number": inv.invoice_number, + "amount_due": inv.amount_due, + "days_past_due": (today - inv.date_due).days, + "SITE_NAME": settings.SITE_NAME, + }, + ) # send notification to accountatnt recipients = ( @@ -90,24 +97,28 @@ class Command(BaseCommand): ) for rec in recipients: Notification.objects.create( - user=rec, - message=_( - """ + user=rec, + message=_( + """ Invoice {invoice_number} is past due,please your View. """ - ).format( - invoice_number=inv.invoice_number, - url=reverse( - "invoice_detail", - kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk}, + ).format( + invoice_number=inv.invoice_number, + url=reverse( + "invoice_detail", + kwargs={ + "dealer_slug": dealer.slug, + "entity_slug": dealer.entity.slug, + "pk": inv.pk, + }, ), ), ) # send email to customer send_email( - 'noreply@yourdomain.com', + "noreply@yourdomain.com", inv.customer.email, subject, message, @@ -131,4 +142,4 @@ class Command(BaseCommand): # created__lt=cutoff, # status=Order.STATUS.NEW # ).delete() - # self.stdout.write(f"Cleaned up {count} old incomplete orders") \ No newline at end of file + # self.stdout.write(f"Cleaned up {count} old incomplete orders") diff --git a/inventory/management/commands/led.py b/inventory/management/commands/led.py index 0b75e47e..0e6b7d44 100644 --- a/inventory/management/commands/led.py +++ b/inventory/management/commands/led.py @@ -2,9 +2,11 @@ from decimal import Decimal import random from django.core.management.base import BaseCommand from inventory.models import Car -from django_ledger.models import EntityModel,InvoiceModel,ItemModel +from django_ledger.models import EntityModel, InvoiceModel, ItemModel from inventory.utils import CarFinanceCalculator from rich import print + + class Command(BaseCommand): help = "" @@ -14,27 +16,43 @@ class Command(BaseCommand): admin = e.admin # estimate = e.get_estimates().first() # e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30") - i=InvoiceModel.objects.first() + i = InvoiceModel.objects.first() - calc = CarFinanceCalculator(i) - data = calc.get_finance_data() - for car_data in data['cars']: - car = i.get_itemtxs_data()[0].filter( - item_model__car__vin=car_data['vin'] - ).first().item_model.car + calc = CarFinanceCalculator(i) + data = calc.get_finance_data() + for car_data in data["cars"]: + car = ( + i.get_itemtxs_data()[0] + .filter(item_model__car__vin=car_data["vin"]) + .first() + .item_model.car + ) print("car", car) - qty = Decimal(car_data['quantity']) + qty = Decimal(car_data["quantity"]) print("qty", qty) # amounts from calculator - net_car_price = Decimal(car_data['total']) # after discount - net_add_price = Decimal(data['total_additionals']) # per car or split however you want - vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty + net_car_price = Decimal(car_data["total"]) # after discount + net_add_price = Decimal( + data["total_additionals"] + ) # per car or split however you want + vat_amount = Decimal(data["total_vat_amount"]) * qty # prorate if multi-qty # grand_total = net_car_price + net_add_price + vat_amount - grand_total = Decimal(data['grand_total']) - cost_total = Decimal(car_data['cost_price']) * qty + grand_total = Decimal(data["grand_total"]) + cost_total = Decimal(car_data["cost_price"]) * qty - print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total) + print( + "net_car_price", + net_car_price, + "net_add_price", + net_add_price, + "vat_amount", + vat_amount, + "grand_total", + grand_total, + "cost_total", + cost_total, + ) # acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)") # acc_sales = e.get_coa_accounts().get(name="Car Sales") @@ -76,4 +94,4 @@ class Command(BaseCommand): # operation=InvoiceModel.ITEMIZE_APPEND) # print(i.amount_due) - # i.save() \ No newline at end of file + # i.save() diff --git a/inventory/management/commands/p.py b/inventory/management/commands/p.py index 468a4223..733c9c08 100644 --- a/inventory/management/commands/p.py +++ b/inventory/management/commands/p.py @@ -2,8 +2,11 @@ from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model import datetime from inventory.models import Dealer -from plans.models import Plan, Order,PlanPricing +from plans.models import Plan, Order, PlanPricing + User = get_user_model() + + class Command(BaseCommand): help = "" @@ -25,4 +28,4 @@ class Command(BaseCommand): ) order.complete_order() - print(user.userplan) \ No newline at end of file + print(user.userplan) diff --git a/inventory/management/commands/plans_maintenance.py b/inventory/management/commands/plans_maintenance.py index 9e201e64..c83ea5bd 100644 --- a/inventory/management/commands/plans_maintenance.py +++ b/inventory/management/commands/plans_maintenance.py @@ -11,6 +11,7 @@ from inventory.tasks import send_bilingual_reminder, handle_email_result logger = logging.getLogger(__name__) + class Command(BaseCommand): help = "Handles subscription plan maintenance tasks" @@ -30,17 +31,18 @@ class Command(BaseCommand): def send_expiration_reminders(self): """Queue email reminders for expiring plans""" - reminder_days = getattr(settings, 'PLANS_EXPIRATION_REMIND', [3, 7, 14]) + reminder_days = getattr(settings, "PLANS_EXPIRATION_REMIND", [3, 7, 14]) today = timezone.now().date() for days in reminder_days: target_date = today + timedelta(days=days) expiring_plans = UserPlan.objects.filter( - active=True, - expire=target_date - ).select_related('user', 'plan') + active=True, expire=target_date + ).select_related("user", "plan") - self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans") + self.stdout.write( + f"Queuing {days}-day reminders for {expiring_plans.count()} plans" + ) for user_plan in expiring_plans: # Queue email task @@ -50,14 +52,13 @@ class Command(BaseCommand): user_plan.plan_id, user_plan.expire, days, - hook=handle_email_result + hook=handle_email_result, ) def deactivate_expired_plans(self): """Deactivate plans that have expired (synchronous)""" expired_plans = UserPlan.objects.filter( - active=True, - expire__lt=timezone.now().date() + active=True, expire__lt=timezone.now().date() ) count = expired_plans.update(active=False) self.stdout.write(f"Deactivated {count} expired plans") @@ -66,7 +67,6 @@ class Command(BaseCommand): """Delete incomplete orders older than 30 days""" cutoff = timezone.now() - timedelta(days=30) count, _ = Order.objects.filter( - created__lt=cutoff, - status=Order.STATUS.NEW + created__lt=cutoff, status=Order.STATUS.NEW ).delete() - self.stdout.write(f"Cleaned up {count} old incomplete orders") \ No newline at end of file + self.stdout.write(f"Cleaned up {count} old incomplete orders") diff --git a/inventory/management/commands/run1.py b/inventory/management/commands/run1.py index 35238c1d..fabfe19c 100644 --- a/inventory/management/commands/run1.py +++ b/inventory/management/commands/run1.py @@ -5,5 +5,10 @@ from django_q.tasks import async_task, result class Command(BaseCommand): def handle(self, *args, **kwargs): from inventory.models import Dealer + instance = Dealer.objects.first() - async_task(func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results") \ No newline at end of file + async_task( + func="inventory.tasks.test_task", + dealer=instance, + hook="inventory.hooks.print_results", + ) diff --git a/inventory/management/commands/seed.py b/inventory/management/commands/seed.py index 14f97f88..5fe97da0 100644 --- a/inventory/management/commands/seed.py +++ b/inventory/management/commands/seed.py @@ -3,21 +3,24 @@ import json, random, string, decimal from django.core.management.base import BaseCommand from django.test import Client from django.contrib.auth import get_user_model -from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan +from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan from inventory.tasks import create_user_dealer -from inventory import models # adjust import to your app +from inventory import models # adjust import to your app from django_q.tasks import async_task User = get_user_model() + class Command(BaseCommand): help = "Seed a full dealership via the real signup & downstream views" def add_arguments(self, parser): - parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed') + parser.add_argument( + "--count", type=int, default=1, help="Number of dealers to seed" + ) def handle(self, *args, **opts): - count = opts['count'] + count = opts["count"] client = Client() # lives inside management command for n in range(6, 9): @@ -43,7 +46,16 @@ class Command(BaseCommand): "address": f"Street {n}, Riyadh", } - dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address']) + dealer = create_user_dealer( + payload["email"], + payload["password"], + payload["name"], + payload["arabic_name"], + payload["phone_number"], + payload["crn"], + payload["vrn"], + payload["address"], + ) user = dealer.user self._assign_random_plan(user) self._services(dealer) @@ -61,7 +73,7 @@ class Command(BaseCommand): return payload["email"] - def _assign_random_plan(self,user): + def _assign_random_plan(self, user): """ Pick a random Plan and create + initialize a UserPlan for the user. """ @@ -72,14 +84,13 @@ class Command(BaseCommand): plan = random.choice(plans) user_plan, created = UserPlan.objects.get_or_create( - user=user, - defaults={'plan': plan, 'active': True} + user=user, defaults={"plan": plan, "active": True} ) if created: user_plan.initialize() return user_plan - def _services(self,dealer): + def _services(self, dealer): additional_services = [ { "name": "Vehicle registration transfer assistance", @@ -114,5 +125,5 @@ class Command(BaseCommand): price=additional_service["price"], description=additional_service["description"], dealer=dealer, - uom="Unit" + uom="Unit", ) diff --git a/inventory/management/commands/seed1.py b/inventory/management/commands/seed1.py index a7e7d9e1..63bc17dd 100644 --- a/inventory/management/commands/seed1.py +++ b/inventory/management/commands/seed1.py @@ -4,11 +4,31 @@ import json, random, string, decimal from django.core.management.base import BaseCommand from django.test import Client from django.contrib.auth import get_user_model -from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan +from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan from inventory.services import decodevin from inventory.tasks import create_user_dealer -from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff -from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel +from inventory.models import ( + AdditionalServices, + Car, + CarColors, + CarFinance, + CarMake, + CustomGroup, + Customer, + Dealer, + ExteriorColors, + InteriorColors, + Lead, + UnitOfMeasure, + Vendor, + Staff, +) +from django_ledger.models import ( + PurchaseOrderModel, + ItemTransactionModel, + ItemModel, + EntityModel, +) from django_q.tasks import async_task from faker import Faker from appointment.models import Appointment, AppointmentRequest, Service, StaffMember @@ -16,6 +36,7 @@ from appointment.models import Appointment, AppointmentRequest, Service, StaffMe User = get_user_model() fake = Faker() + class Command(BaseCommand): help = "Seed a full dealership via the real signup & downstream views" @@ -31,7 +52,6 @@ class Command(BaseCommand): # self._create_randome_services(dealer) # self._create_random_lead(dealer) - # dealer = Dealer.objects.get(name="Dealer #6") # coa_model = dealer.entity.get_default_coa() # inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)") @@ -43,20 +63,32 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}")) def _create_random_po(self, dealer): - for i in range(random.randint(1,70)): + for i in range(random.randint(1, 70)): try: e: EntityModel = dealer.entity - e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}") + e.create_purchase_order( + po_title=f"Test PO {random.randint(1, 9999)}-{i}" + ) except Exception as e: self.stderr.write(self.style.ERROR(f"Error : {e}")) def _create_random_vendors(self, dealer): - for i in range(random.randint(1,50)): + for i in range(random.randint(1, 50)): try: name = fake.name() - n = random.randint(1,9999) - phone = f"05678{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}" - Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}") + n = random.randint(1, 9999) + phone = f"05678{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}" + Vendor.objects.create( + dealer=dealer, + name=f"{name}{n}", + arabic_name=f"{name}{n}", + email=f"{name}{n}@tenhal.sa", + phone_number=phone, + crn=f"CRN {n}", + vrn=f"VRN {n}", + address=f"Address {fake.address()}", + contact_person=f"Contact Person {name}{n}", + ) except Exception as e: pass @@ -65,7 +97,9 @@ class Command(BaseCommand): name = f"{fake.name()}{i}" email = fake.email() password = "Tenhal@123" - user = User.objects.create_user(username=email, email=email, password=password) + user = User.objects.create_user( + username=email, email=email, password=password + ) user.is_staff = True user.save() @@ -74,17 +108,24 @@ class Command(BaseCommand): # for service in services: # staff_member.services_offered.add(service) - staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True) + staff = Staff.objects.create( + dealer=dealer, + user=user, + name=name, + arabic_name=name, + phone_number=fake.phone_number(), + active=True, + ) groups = CustomGroup.objects.filter(dealer=dealer) random_group = random.choice(list(groups)) staff.add_group(random_group.group) # for i in range(random.randint(1,15)): - # n = random.randint(1,9999) - # phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}" - # Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}") + # n = random.randint(1,9999) + # phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}" + # Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}") - def _create_random_cars(self,dealer): + def _create_random_cars(self, dealer): vendors = Vendor.objects.filter(dealer=dealer).all() vin_list = [ @@ -103,18 +144,20 @@ class Command(BaseCommand): ] for vin in vin_list: try: - for _ in range(random.randint(1,2)): + for _ in range(random.randint(1, 2)): vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}" result = decodevin(vin) make = CarMake.objects.get(name=result["maker"]) - model = make.carmodel_set.filter(name__contains=result["model"]).first() + model = make.carmodel_set.filter( + name__contains=result["model"] + ).first() if not model or model == "": model = random.choice(make.carmodel_set.all()) year = result["modelYear"] serie = random.choice(model.carserie_set.all()) trim = random.choice(serie.cartrim_set.all()) vendor = random.choice(vendors) - print(make, model, serie, trim, vendor,vin) + print(make, model, serie, trim, vendor, vin) car = Car.objects.create( vin=vin, id_car_make=make, @@ -128,9 +171,12 @@ class Command(BaseCommand): mileage=0, ) print(car) - cp=random.randint(10000, 100000) + cp = random.randint(10000, 100000) CarFinance.objects.create( - car=car, cost_price=cp, selling_price=0,marked_price=cp+random.randint(2000, 7000) + car=car, + cost_price=cp, + selling_price=0, + marked_price=cp + random.randint(2000, 7000), ) CarColors.objects.create( car=car, @@ -141,8 +187,8 @@ class Command(BaseCommand): except Exception as e: print(e) - def _create_random_customers(self,dealer): - for i in range(random.randint(1,60)): + def _create_random_customers(self, dealer): + for i in range(random.randint(1, 60)): try: c = Customer( dealer=dealer, @@ -161,7 +207,7 @@ class Command(BaseCommand): except Exception as e: pass - def _create_randome_services(self,dealer): + def _create_randome_services(self, dealer): additional_services = [ { "name": "Vehicle registration transfer assistance", @@ -196,12 +242,11 @@ class Command(BaseCommand): price=additional_service["price"], description=additional_service["description"], dealer=dealer, - uom=uom + uom=uom, ) - - def _create_random_lead(self,dealer): - for i in range(random.randint(1,60)): + def _create_random_lead(self, dealer): + for i in range(random.randint(1, 60)): try: first_name = fake.name() last_name = fake.last_name() @@ -224,7 +269,7 @@ class Command(BaseCommand): id_car_model=model, source="website", channel="website", - staff=staff + staff=staff, ) c = Customer( dealer=dealer, @@ -243,4 +288,4 @@ class Command(BaseCommand): lead.customer = c lead.save() except Exception as e: - pass \ No newline at end of file + pass diff --git a/inventory/middleware.py b/inventory/middleware.py index 4de7968a..cbd4c3a5 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -152,11 +152,22 @@ class DealerSlugMiddleware: def process_view(self, request, view_func, view_args, view_kwargs): paths = [ - "/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/", - "/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/", - "/en/notifications/", "/ar/notifications/", "/en/appointment/", - "/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/", - "/ar/help_center/", "/en/help_center/", + "/ar/signup/", + "/en/signup/", + "/ar/login/", + "/en/login/", + "/ar/logout/", + "/en/logout/", + "/en/ledger/", + "/ar/ledger/", + "/en/notifications/", + "/ar/notifications/", + "/en/appointment/", + "/ar/appointment/", + "/en/feature/recall/", + "/ar/feature/recall/", + "/ar/help_center/", + "/en/help_center/", ] print("------------------------------------") print(request.path in paths) diff --git a/inventory/models.py b/inventory/models.py index 87330e1b..d77a1d52 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -42,12 +42,14 @@ from django_ledger.models import ( from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.serializers.json import DjangoJSONEncoder + # from appointment.models import StaffMember from plans.quota import get_user_quota from plans.models import UserPlan from django.db.models import Q from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFill + # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords from plans.models import Invoice @@ -229,7 +231,9 @@ class CarMake(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, blank=True, null=True) slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) - logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.jpg") + logo = models.ImageField( + _("logo"), upload_to="car_make", blank=True, null=True, default="user-logo.jpg" + ) is_sa_import = models.BooleanField(default=False) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) @@ -589,7 +593,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin): "price_": str(self.price_), "taxable": self.taxable, "uom": self.uom, - "service_tax":str(self.service_tax) + "service_tax": str(self.service_tax), } @property @@ -604,9 +608,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin): @property def service_tax(self): vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first() - return ( - Decimal(self.price * vat.rate) - ) + return Decimal(self.price * vat.rate) class Meta: verbose_name = _("Additional Services") @@ -683,10 +685,13 @@ class Car(Base): ) # additional_services = models.ManyToManyField( - AdditionalServices, related_name="additionals", blank=True,null=True + AdditionalServices, related_name="additionals", blank=True, null=True ) cost_price = models.DecimalField( - max_digits=14, decimal_places=2, verbose_name=_("Cost Price"),default=Decimal("0.00") + max_digits=14, + decimal_places=2, + verbose_name=_("Cost Price"), + default=Decimal("0.00"), ) selling_price = models.DecimalField( max_digits=14, @@ -710,7 +715,7 @@ class Car(Base): remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) - sold_date=models.DateTimeField(verbose_name=_("Sold Date"),null=True,blank=True) + sold_date = models.DateTimeField(verbose_name=_("Sold Date"), null=True, blank=True) hash = models.CharField( max_length=64, blank=True, null=True, verbose_name=_("Hash") ) @@ -773,6 +778,7 @@ class Car(Base): @property def logo(self): return getattr(self.id_car_make, "logo", "") + # @property # def additional_services(self): # return self.additional_services.all() @@ -787,9 +793,15 @@ class Car(Base): ) except Exception: return False + @property def invoice(self): - return self.item_model.invoicemodel_set.first if self.item_model.invoicemodel_set.first() else None + return ( + self.item_model.invoicemodel_set.first + if self.item_model.invoicemodel_set.first() + else None + ) + def get_transfer(self): return self.transfer_logs.filter(active=True).first() @@ -873,39 +885,54 @@ class Car(Base): car=self, exterior=exterior, interior=interior ) self.save() + @property def logo(self): return self.id_car_make.logo.url if self.id_car_make.logo else None + # @property def get_additional_services_amount(self): return sum([Decimal(x.price) for x in self.additional_services.all()]) + @property def get_additional_services_amount_(self): return sum([Decimal(x.price_) for x in self.additional_services.all()]) + @property def get_additional_services_vat(self): - vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first() - return sum([Decimal((x.price)*(vat.rate)) for x in self.additional_services.filter(taxable=True)]) + vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first() + return sum( + [ + Decimal((x.price) * (vat.rate)) + for x in self.additional_services.filter(taxable=True) + ] + ) def get_additional_services(self): - vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first() - return {"services": [[x,((x.price)*(vat.rate) if x.taxable else 0)] for x in self.additional_services.all()], - "total_":self.get_additional_services_amount_, - "total":self.get_additional_services_amount, - "services_vat":self.get_additional_services_vat} + vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first() + return { + "services": [ + [x, ((x.price) * (vat.rate) if x.taxable else 0)] + for x in self.additional_services.all() + ], + "total_": self.get_additional_services_amount_, + "total": self.get_additional_services_amount, + "services_vat": self.get_additional_services_vat, + } @property def final_price(self): - return Decimal(self.marked_price -self.discount) + return Decimal(self.marked_price - self.discount) + @property def vat_amount(self): - vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first() + vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first() return Decimal(self.final_price) * (vat.rate) @property def total_services_and_car_vat(self): - return self.vat_amount+self.get_additional_services()['services_vat'] + return self.vat_amount + self.get_additional_services()["services_vat"] @property def final_price_plus_vat(self): @@ -913,29 +940,32 @@ class Car(Base): @property def final_price_plus_services_plus_vat(self): - return Decimal(self.final_price_plus_vat) + Decimal(self.get_additional_services()['total_']) #total services with vat and car_sell price with vat + return Decimal(self.final_price_plus_vat) + Decimal( + self.get_additional_services()["total_"] + ) # total services with vat and car_sell price with vat + # to be used after invoice is created @property def invoice(self): return self.item_model.invoicemodel_set.first() or None + @property def estimate(self): - return getattr(self.invoice,'ce_model',None) + return getattr(self.invoice, "ce_model", None) + @property def discount(self): if not self.estimate: return 0 try: instance = ExtraInfo.objects.get( - dealer=self.dealer, - content_type=ContentType.objects.get_for_model(EstimateModel), - object_id=self.estimate.pk, - ) - return Decimal(instance.data.get('discount',0)) + dealer=self.dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=self.estimate.pk, + ) + return Decimal(instance.data.get("discount", 0)) except ExtraInfo.DoesNotExist: - return Decimal(0) - - + return Decimal(0) # def get_discount_amount(self,estimate,user): # try: @@ -961,10 +991,6 @@ class Car(Base): # return round(self.total_discount + self.vat_amount + self.total_additionals, 2) - - - - class CarTransfer(models.Model): car = models.ForeignKey( "Car", @@ -1001,7 +1027,7 @@ class CarTransfer(models.Model): @property def total_price(self): - return self.quantity * self.car.total_vat # TODO : check later + return self.quantity * self.car.total_vat # TODO : check later class Meta: verbose_name = _("Car Transfer Log") @@ -1311,7 +1337,11 @@ class Dealer(models.Model, LocalizedNameMixin): ) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) name = models.CharField(max_length=255, verbose_name=_("English Name")) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) @@ -1365,6 +1395,7 @@ class Dealer(models.Model, LocalizedNameMixin): @property def customers(self): return models.Customer.objects.filter(dealer=self) + @property def user_quota(self): try: @@ -1415,6 +1446,7 @@ class Dealer(models.Model, LocalizedNameMixin): def invoices(self): return Invoice.objects.filter(order__user=self.user) + class StaffTypes(models.TextChoices): # MANAGER = "manager", _("Manager") INVENTORY = "inventory", _("Inventory") @@ -1429,15 +1461,17 @@ class Staff(models.Model): # staff_member = models.OneToOneField( # StaffMember, on_delete=models.CASCADE, related_name="staff" # ) - user = models.OneToOneField( - User, on_delete=models.CASCADE, related_name="staff" - ) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff") 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")) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) staff_type = models.CharField( choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type") ) @@ -1445,7 +1479,11 @@ class Staff(models.Model): max_length=200, blank=True, null=True, verbose_name=_("Address") ) logo = models.ImageField( - upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="default-image/user.jpg" + upload_to="logos/staff", + blank=True, + null=True, + verbose_name=_("Image"), + default="default-image/user.jpg", ) thumbnail = ImageSpecField( source="logo", @@ -1480,6 +1518,7 @@ class Staff(models.Model): @property def fullname(self): return self.first_name + " " + self.last_name + def deactivate_account(self): self.active = False self.user.is_active = False @@ -1544,8 +1583,7 @@ class Staff(models.Model): permissions = [] constraints = [ models.UniqueConstraint( - fields=['dealer', 'user'], - name='unique_staff_email_per_dealer' + fields=["dealer", "user"], name="unique_staff_email_per_dealer" ) ] @@ -1648,7 +1686,11 @@ class Customer(models.Model): CustomerModel, on_delete=models.SET_NULL, null=True ) user = models.OneToOneField( - User, on_delete=models.CASCADE, related_name="customer_profile", null=True, blank=True + User, + on_delete=models.CASCADE, + related_name="customer_profile", + null=True, + blank=True, ) title = models.CharField( choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") @@ -1676,7 +1718,11 @@ class Customer(models.Model): ) active = models.BooleanField(default=True, verbose_name=_("Active")) image = models.ImageField( - upload_to="customers/", blank=True, null=True, verbose_name=_("Image"),default="default-image/user-jpg" + upload_to="customers/", + blank=True, + null=True, + verbose_name=_("Image"), + default="default-image/user-jpg", ) thumbnail = ImageSpecField( source="image", @@ -1708,8 +1754,7 @@ class Customer(models.Model): class Meta: constraints = [ models.UniqueConstraint( - fields=['dealer', 'email'], - name='unique_customer_email_per_dealer' + fields=["dealer", "email"], name="unique_customer_email_per_dealer" ) ] verbose_name = _("Customer") @@ -1779,13 +1824,13 @@ class Customer(models.Model): user, created = User.objects.get_or_create( username=self.email, defaults={ - 'email': self.email, - 'first_name': self.first_name, - 'last_name': self.last_name, - 'password': make_random_password(), - 'is_staff': False, - 'is_superuser': False, - 'is_active': False if for_lead else True, + "email": self.email, + "first_name": self.first_name, + "last_name": self.last_name, + "password": make_random_password(), + "is_staff": False, + "is_superuser": False, + "is_active": False if for_lead else True, }, ) self.user = user @@ -1822,7 +1867,11 @@ class Organization(models.Model, LocalizedNameMixin): CustomerModel, on_delete=models.SET_NULL, null=True ) user = models.OneToOneField( - User, on_delete=models.CASCADE, related_name="organization_profile", null=True, blank=True + User, + on_delete=models.CASCADE, + related_name="organization_profile", + null=True, + blank=True, ) name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) @@ -1831,12 +1880,20 @@ class Organization(models.Model, LocalizedNameMixin): ) vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) email = models.EmailField(verbose_name=_("Email")) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) logo = models.ImageField( - upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg" + upload_to="logos", + blank=True, + null=True, + verbose_name=_("Logo"), + default="default-image/user.jpg", ) thumbnail = ImageSpecField( source="logo", @@ -1965,7 +2022,11 @@ class Representative(models.Model, LocalizedNameMixin): id_number = models.CharField( max_length=10, unique=True, verbose_name=_("ID Number") ) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") @@ -1985,7 +2046,11 @@ class Lead(models.Model): first_name = models.CharField(max_length=50, verbose_name=_("First Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) email = models.EmailField(verbose_name=_("Email")) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) address = models.CharField( max_length=200, blank=True, null=True, verbose_name=_("Address") ) @@ -2175,8 +2240,9 @@ class Lead(models.Model): .order_by("-updated") .first() ) + def get_absolute_url(self): - return reverse("lead_detail", args=[self.dealer.slug,self.slug]) + return reverse("lead_detail", args=[self.dealer.slug, self.slug]) def save(self, *args, **kwargs): if not self.slug: @@ -2246,6 +2312,7 @@ class Schedule(models.Model): @property def duration(self): return (self.end_time - self.start_time).seconds + @property def schedule_past_date(self): if self.scheduled_at < now(): @@ -2255,6 +2322,7 @@ class Schedule(models.Model): @property def get_purpose(self): return self.purpose.replace("_", " ").title() + class Meta: ordering = ["-scheduled_at"] verbose_name = _("Schedule") @@ -2673,11 +2741,19 @@ class Vendor(models.Model, LocalizedNameMixin): arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) name = models.CharField(max_length=255, verbose_name=_("English Name")) contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) - phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()]) + phone_number = models.CharField( + max_length=255, + verbose_name=_("Phone Number"), + validators=[SaudiPhoneNumberValidator()], + ) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField(max_length=200, verbose_name=_("Address")) logo = models.ImageField( - upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg" + upload_to="logos/vendors", + blank=True, + null=True, + verbose_name=_("Logo"), + default="default-image/user.jpg", ) thumbnail = ImageSpecField( source="logo", @@ -3225,7 +3301,6 @@ class CustomGroup(models.Model): "activity", "payment", "vendor", - ], other_perms=[ "view_car", @@ -3236,8 +3311,7 @@ class CustomGroup(models.Model): "view_saleorder", "view_leads", "view_opportunity", - 'view_customer' - + "view_customer", ], ) self.set_permissions( @@ -3533,7 +3607,7 @@ class ExtraInfo(models.Model): return f"ExtraInfo for {self.content_object} ({self.content_type})" @classmethod - def get_sale_orders(cls, staff=None, is_dealer=False,dealer=None): + def get_sale_orders(cls, staff=None, is_dealer=False, dealer=None): if not staff and not is_dealer: return [] @@ -3546,11 +3620,13 @@ class ExtraInfo(models.Model): content_type=content_type, related_content_type=related_content_type, related_object_id__isnull=False, - ).union(cls.objects.filter( - dealer=dealer, - content_type=ContentType.objects.get_for_model(EstimateModel), - related_content_type=ContentType.objects.get_for_model(User), - )) + ).union( + cls.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + ) + ) else: qs = cls.objects.filter( dealer=dealer, @@ -3559,7 +3635,17 @@ class ExtraInfo(models.Model): related_object_id=staff.pk, ) # qs = qs.select_related("customer","estimate","invoice") - data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()]) + data = SaleOrder.objects.filter( + pk__in=[ + x.content_object.sale_orders.select_related( + "customer", "estimate", "invoice" + ) + .first() + .pk + for x in qs + if x.content_object.sale_orders.first() + ] + ) return data @@ -3572,7 +3658,7 @@ class ExtraInfo(models.Model): # ] @classmethod - def get_invoices(cls, staff=None, is_dealer=False,dealer=None): + def get_invoices(cls, staff=None, is_dealer=False, dealer=None): if not staff and not is_dealer: return [] @@ -3585,11 +3671,13 @@ class ExtraInfo(models.Model): content_type=content_type, related_content_type=related_content_type, related_object_id__isnull=False, - ).union(cls.objects.filter( - dealer=dealer, - content_type=content_type, - related_content_type=ContentType.objects.get_for_model(User), - )) + ).union( + cls.objects.filter( + dealer=dealer, + content_type=content_type, + related_content_type=ContentType.objects.get_for_model(User), + ) + ) else: qs = cls.objects.filter( dealer=dealer, @@ -3608,32 +3696,16 @@ class Recall(models.Model): title = models.CharField(max_length=200, verbose_name=_("Recall Title")) description = models.TextField(verbose_name=_("Description")) make = models.ForeignKey( - CarMake, - models.DO_NOTHING, - verbose_name=_("Make"), - null=True, - blank=True + CarMake, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True ) model = models.ForeignKey( - CarModel, - models.DO_NOTHING, - verbose_name=_("Model"), - null=True, - blank=True + CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True ) serie = models.ForeignKey( - CarSerie, - models.DO_NOTHING, - verbose_name=_("Series"), - null=True, - blank=True + CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True ) trim = models.ForeignKey( - CarTrim, - models.DO_NOTHING, - verbose_name=_("Trim"), - null=True, - blank=True + CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True ) year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True) year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True) @@ -3643,7 +3715,7 @@ class Recall(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - verbose_name=_("Created By") + verbose_name=_("Created By"), ) class Meta: @@ -3653,11 +3725,16 @@ class Recall(models.Model): def __str__(self): return self.title + class RecallNotification(models.Model): - recall = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications') - dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_notifications') + recall = models.ForeignKey( + Recall, on_delete=models.CASCADE, related_name="notifications" + ) + dealer = models.ForeignKey( + "Dealer", on_delete=models.CASCADE, related_name="recall_notifications" + ) sent_at = models.DateTimeField(auto_now_add=True) - cars_affected = models.ManyToManyField(Car, related_name='recall_notifications') + cars_affected = models.ManyToManyField(Car, related_name="recall_notifications") class Meta: verbose_name = _("Recall Notification") @@ -3666,27 +3743,30 @@ class RecallNotification(models.Model): def __str__(self): return f"Notification for {self.dealer} about {self.recall}" + class Ticket(models.Model): STATUS_CHOICES = [ - ('open', 'Open'), - ('in_progress', 'In Progress'), - ('resolved', 'Resolved'), - ('closed', 'Closed'), + ("open", "Open"), + ("in_progress", "In Progress"), + ("resolved", "Resolved"), + ("closed", "Closed"), ] PRIORITY_CHOICES = [ - ('low', 'Low'), - ('medium', 'Medium'), - ('high', 'High'), - ('critical', 'Critical'), + ("low", "Low"), + ("medium", "Medium"), + ("high", "High"), + ("critical", "Critical"), ] - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='tickets') + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="tickets") subject = models.CharField(max_length=200) description = models.TextField() resolution_notes = models.TextField(blank=True, null=True) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') - priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open") + priority = models.CharField( + max_length=20, choices=PRIORITY_CHOICES, default="medium" + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -3697,7 +3777,7 @@ class Ticket(models.Model): Returns None if ticket isn't resolved/closed. Returns timedelta if resolved/closed. """ - if self.status in ['resolved', 'closed'] and self.created_at: + if self.status in ["resolved", "closed"] and self.created_at: return self.updated_at - self.created_at return None @@ -3729,9 +3809,11 @@ class Ticket(models.Model): class CarImage(models.Model): - car = models.OneToOneField('Car', on_delete=models.CASCADE, related_name='generated_image') + car = models.OneToOneField( + "Car", on_delete=models.CASCADE, related_name="generated_image" + ) image_hash = models.CharField(max_length=64, unique=True) - image = models.ImageField(upload_to='car_images/', null=True, blank=True) + image = models.ImageField(upload_to="car_images/", null=True, blank=True) is_generating = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -3752,5 +3834,5 @@ class CarImage(models.Model): async_task( generate_car_image_task, self.id, - task_name=f"generate_car_image_{self.car.vin}" - ) \ No newline at end of file + task_name=f"generate_car_image_{self.car.vin}", + ) diff --git a/inventory/notifications/sse.py b/inventory/notifications/sse.py index 83c32205..dd525d14 100644 --- a/inventory/notifications/sse.py +++ b/inventory/notifications/sse.py @@ -87,6 +87,7 @@ from inventory.models import Notification import asyncio from datetime import datetime + @database_sync_to_async def get_user(user_id): User = get_user_model() @@ -95,24 +96,24 @@ def get_user(user_id): except User.DoesNotExist: return AnonymousUser() + @database_sync_to_async def get_notifications(user, last_id): notifications = Notification.objects.filter( - user=user, - id__gt=last_id, - is_read=False + user=user, id__gt=last_id, is_read=False ).order_by("created") return [ { - 'id': n.id, - 'message': n.message, - 'created': n.created.isoformat(), # Convert datetime to string - 'is_read': n.is_read + "id": n.id, + "message": n.message, + "created": n.created.isoformat(), # Convert datetime to string + "is_read": n.is_read, } for n in notifications ] + class NotificationSSEApp: async def __call__(self, scope, receive, send): if scope["type"] != "http": @@ -143,15 +144,17 @@ class NotificationSSEApp: for notification in notifications: await self._send_notification(send, notification) - if notification['id'] > last_id: - last_id = notification['id'] + if notification["id"] > last_id: + last_id = notification["id"] # Send keep-alive comment every 15 seconds - await send({ - "type": "http.response.body", - "body": b":keep-alive\n\n", - "more_body": True - }) + await send( + { + "type": "http.response.body", + "body": b":keep-alive\n\n", + "more_body": True, + } + ) # await asyncio.sleep(3) @@ -161,16 +164,18 @@ class NotificationSSEApp: await self._close_connection(send) async def _send_headers(self, send): - await send({ - "type": "http.response.start", - "status": 200, - "headers": [ - (b"content-type", b"text/event-stream"), - (b"cache-control", b"no-cache"), - (b"connection", b"keep-alive"), - (b"x-accel-buffering", b"no"), - ] - }) + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"content-type", b"text/event-stream"), + (b"cache-control", b"no-cache"), + (b"connection", b"keep-alive"), + (b"x-accel-buffering", b"no"), + ], + } + ) async def _send_notification(self, send, notification): try: @@ -179,27 +184,25 @@ class NotificationSSEApp: f"event: notification\n" f"data: {json.dumps(notification)}\n\n" ) - await send({ - "type": "http.response.body", - "body": event_str.encode("utf-8"), - "more_body": True - }) + await send( + { + "type": "http.response.body", + "body": event_str.encode("utf-8"), + "more_body": True, + } + ) except Exception as e: print(f"Error sending notification: {e}") async def _send_response(self, send, status, body): - await send({ - "type": "http.response.start", - "status": status, - "headers": [(b"content-type", b"text/plain")] - }) - await send({ - "type": "http.response.body", - "body": body - }) + await send( + { + "type": "http.response.start", + "status": status, + "headers": [(b"content-type", b"text/plain")], + } + ) + await send({"type": "http.response.body", "body": body}) async def _close_connection(self, send): - await send({ - "type": "http.response.body", - "body": b"" - }) \ No newline at end of file + await send({"type": "http.response.body", "body": b""}) diff --git a/inventory/override.py b/inventory/override.py index 0e66e9be..37c59825 100644 --- a/inventory/override.py +++ b/inventory/override.py @@ -20,7 +20,12 @@ from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse -from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel +from django_ledger.models import ( + ItemTransactionModel, + InvoiceModel, + LedgerModel, + EntityModel, +) from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView from django_ledger.forms.chart_of_accounts import ( @@ -35,17 +40,28 @@ from django_ledger.forms.purchase_order import ( get_po_itemtxs_formset_class, ) from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn -from django_ledger.models import PurchaseOrderModel, EstimateModel, BillModel, ChartOfAccountModel +from django_ledger.models import ( + PurchaseOrderModel, + EstimateModel, + BillModel, + ChartOfAccountModel, +) from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import UpdateView from django.views.generic.base import RedirectView from django.views.generic.list import ListView from django.utils.translation import gettext_lazy as _ -from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm, - get_invoice_itemtxs_formset_class, - DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm, - ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm, - AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm) +from django_ledger.forms.invoice import ( + BaseInvoiceModelUpdateForm, + InvoiceModelCreateForEstimateForm, + get_invoice_itemtxs_formset_class, + DraftInvoiceModelUpdateForm, + InReviewInvoiceModelUpdateForm, + ApprovedInvoiceModelUpdateForm, + PaidInvoiceModelUpdateForm, + AccruedAndApprovedInvoiceModelUpdateForm, + InvoiceModelCreateForm, +) logger = logging.getLogger(__name__) @@ -71,7 +87,11 @@ class PurchaseOrderModelUpdateView( context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug - context["po_ready_to_fulfill"] = [item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received'] + context["po_ready_to_fulfill"] = [ + item + for item in po_model.get_itemtxs_data()[0] + if item.po_item_status == "received" + ] if not itemtxs_formset: itemtxs_qs = self.get_po_itemtxs_qs(po_model) @@ -776,12 +796,12 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): - slug_url_kwarg = 'invoice_pk' - slug_field = 'uuid' - context_object_name = 'invoice' + slug_url_kwarg = "invoice_pk" + slug_field = "uuid" + context_object_name = "invoice" # template_name = 'inventory/sales/invoices/invoice_update.html' form_class = BaseInvoiceModelUpdateForm - http_method_names = ['get', 'post'] + http_method_names = ["get", "post"] action_update_items = False @@ -802,115 +822,137 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update def get_form(self, form_class=None): form_class = self.get_form_class() - if self.request.method == 'POST' and self.action_update_items: + if self.request.method == "POST" and self.action_update_items: return form_class( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.dealer.user, - instance=self.object + instance=self.object, ) return form_class( - entity_slug=self.kwargs['entity_slug'], + entity_slug=self.kwargs["entity_slug"], user_model=self.request.dealer.user, - **self.get_form_kwargs() + **self.get_form_kwargs(), ) def get_context_data(self, itemtxs_formset=None, **kwargs): context = super().get_context_data(**kwargs) invoice_model: InvoiceModel = self.object - title = f'Invoice {invoice_model.invoice_number}' - context['page_title'] = title - context['header_title'] = title + title = f"Invoice {invoice_model.invoice_number}" + context["page_title"] = title + context["header_title"] = title ledger_model: LedgerModel = self.object.ledger if not invoice_model.is_configured(): messages.add_message( request=self.request, - message=f'Invoice {invoice_model.invoice_number} must have all accounts configured.', + message=f"Invoice {invoice_model.invoice_number} must have all accounts configured.", level=messages.ERROR, - extra_tags='is-danger' + extra_tags="is-danger", ) if not invoice_model.is_paid(): if ledger_model.locked: - messages.add_message(self.request, - messages.ERROR, - f'Warning! This invoice is locked. Must unlock before making any changes.', - extra_tags='is-danger') + messages.add_message( + self.request, + messages.ERROR, + f"Warning! This invoice is locked. Must unlock before making any changes.", + extra_tags="is-danger", + ) if ledger_model.locked: - messages.add_message(self.request, - messages.ERROR, - f'Warning! This Invoice is Locked. Must unlock before making any changes.', - extra_tags='is-danger') + messages.add_message( + self.request, + messages.ERROR, + f"Warning! This Invoice is Locked. Must unlock before making any changes.", + extra_tags="is-danger", + ) if not ledger_model.is_posted(): - messages.add_message(self.request, - messages.INFO, - f'This Invoice has not been posted. Must post to see ledger changes.', - extra_tags='is-info') + messages.add_message( + self.request, + messages.INFO, + f"This Invoice has not been posted. Must post to see ledger changes.", + extra_tags="is-info", + ) if not itemtxs_formset: - itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model') - itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs) - invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) - itemtxs_formset = invoice_itemtxs_formset_class( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.dealer.user, - invoice_model=invoice_model, + itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related( + "item_model" + ) + itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data( queryset=itemtxs_qs ) + invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class( + invoice_model + ) + itemtxs_formset = invoice_itemtxs_formset_class( + entity_slug=self.kwargs["entity_slug"], + user_model=self.request.dealer.user, + invoice_model=invoice_model, + queryset=itemtxs_qs, + ) else: - itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset) + itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data( + queryset=itemtxs_formset.queryset + ) - context['itemtxs_formset'] = itemtxs_formset - context['total_amount__sum'] = itemtxs_agg['total_amount__sum'] + context["itemtxs_formset"] = itemtxs_formset + context["total_amount__sum"] = itemtxs_agg["total_amount__sum"] return context def get_success_url(self): - entity_slug = self.kwargs['entity_slug'] - invoice_pk = self.kwargs['invoice_pk'] - return reverse('invoice_detail', - kwargs={ - 'dealer_slug': self.request.dealer.slug, - 'entity_slug': entity_slug, - 'pk': invoice_pk - }) + entity_slug = self.kwargs["entity_slug"] + invoice_pk = self.kwargs["invoice_pk"] + return reverse( + "invoice_detail", + kwargs={ + "dealer_slug": self.request.dealer.slug, + "entity_slug": entity_slug, + "pk": invoice_pk, + }, + ) # def get_queryset(self): # qs = super().get_queryset() # return qs.prefetch_related('itemtransactionmodel_set') def get_queryset(self): if self.queryset is None: - self.queryset = InvoiceModel.objects.for_entity( - entity_slug=self.kwargs['entity_slug'], - user_model=self.request.user - ).select_related('customer', 'ledger').order_by('-created') - return super().get_queryset().prefetch_related('itemtransactionmodel_set') - + self.queryset = ( + InvoiceModel.objects.for_entity( + entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + ) + .select_related("customer", "ledger") + .order_by("-created") + ) + return super().get_queryset().prefetch_related("itemtransactionmodel_set") def form_valid(self, form): invoice_model: InvoiceModel = form.save(commit=False) if invoice_model.can_migrate(): invoice_model.migrate_state( user_model=self.request.dealer.user, - entity_slug=self.kwargs['entity_slug'] + entity_slug=self.kwargs["entity_slug"], ) - messages.add_message(self.request, - messages.SUCCESS, - f'Invoice {self.object.invoice_number} successfully updated.', - extra_tags='is-success') + messages.add_message( + self.request, + messages.SUCCESS, + f"Invoice {self.object.invoice_number} successfully updated.", + extra_tags="is-success", + ) return super().form_valid(form) def get(self, request, entity_slug, invoice_pk, *args, **kwargs): if self.action_update_items: return HttpResponseRedirect( - redirect_to=reverse('invoice_update', - kwargs={ - 'dealer_slug': request.dealer.slug, - 'entity_slug': entity_slug, - 'pk': invoice_pk - }) + redirect_to=reverse( + "invoice_update", + kwargs={ + "dealer_slug": request.dealer.slug, + "entity_slug": entity_slug, + "pk": invoice_pk, + }, + ) ) return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs) @@ -922,18 +964,22 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update queryset = self.get_queryset() invoice_model = self.get_object(queryset=queryset) self.object = invoice_model - invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) - itemtxs_formset = invoice_itemtxs_formset_class(request.POST, - user_model=self.request.dealer.user, - invoice_model=invoice_model, - entity_slug=entity_slug) + invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class( + invoice_model + ) + itemtxs_formset = invoice_itemtxs_formset_class( + request.POST, + user_model=self.request.dealer.user, + invoice_model=invoice_model, + entity_slug=entity_slug, + ) if not invoice_model.can_edit_items(): messages.add_message( request, - message=f'Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}', + message=f"Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}", level=messages.ERROR, - extra_tags='is-danger' + extra_tags="is-danger", ) context = self.get_context_data(itemtxs_formset=itemtxs_formset) return self.render_to_response(context=context) @@ -941,8 +987,12 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update if itemtxs_formset.has_changed(): if itemtxs_formset.is_valid(): itemtxs_list = itemtxs_formset.save(commit=False) - entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user) - entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug) + entity_qs = EntityModel.objects.for_user( + user_model=self.request.dealer.user + ) + entity_model: EntityModel = get_object_or_404( + entity_qs, slug__exact=entity_slug + ) for itemtxs in itemtxs_list: itemtxs.invoice_model_id = invoice_model.uuid @@ -953,53 +1003,72 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update invoice_model.get_state(commit=True) invoice_model.clean() invoice_model.save( - update_fields=['amount_due', - 'amount_receivable', - 'amount_unearned', - 'amount_earned', - 'updated'] + update_fields=[ + "amount_due", + "amount_receivable", + "amount_unearned", + "amount_earned", + "updated", + ] ) invoice_model.migrate_state( entity_slug=entity_slug, user_model=self.request.user, raise_exception=False, - itemtxs_qs=itemtxs_qs + itemtxs_qs=itemtxs_qs, ) - messages.add_message(request, - message=f'Items for Invoice {invoice_model.invoice_number} saved.', - level=messages.SUCCESS, - extra_tags='is-success') + messages.add_message( + request, + message=f"Items for Invoice {invoice_model.invoice_number} saved.", + level=messages.SUCCESS, + extra_tags="is-success", + ) return HttpResponseRedirect( - redirect_to=reverse('django_ledger:invoice-update', - kwargs={ - 'entity_slug': entity_slug, - 'invoice_pk': invoice_pk - }) + redirect_to=reverse( + "django_ledger:invoice-update", + kwargs={ + "entity_slug": entity_slug, + "invoice_pk": invoice_pk, + }, + ) ) # if not valid, return formset with errors... - return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset)) + return self.render_to_response( + context=self.get_context_data(itemtxs_formset=itemtxs_formset) + ) return super(InvoiceModelUpdateView, self).post(request, **kwargs) - -class ChartOfAccountModelModelBaseViewMixIn(LoginRequiredMixin, PermissionRequiredMixin): +class ChartOfAccountModelModelBaseViewMixIn( + LoginRequiredMixin, PermissionRequiredMixin +): queryset = None permission_required = [] + def get_queryset(self): if self.queryset is None: entity_model = self.request.dealer.entity - self.queryset = entity_model.chartofaccountmodel_set.all().order_by('-updated') + self.queryset = entity_model.chartofaccountmodel_set.all().order_by( + "-updated" + ) return super().get_queryset() + def get_redirect_url(self, *args, **kwargs): - return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, - 'entity_slug': self.request.entity.slug}) + return reverse( + "coa-list", + kwargs={ + "dealer_slug": self.request.dealer.slug, + "entity_slug": self.request.entity.slug, + }, + ) + class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListView): - template_name = 'chart_of_accounts/coa_list.html' - context_object_name = 'coa_list' + template_name = "chart_of_accounts/coa_list.html" + context_object_name = "coa_list" inactive = False def get_queryset(self): @@ -1010,84 +1079,116 @@ class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListVie def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=None, **kwargs) - context['inactive'] = self.inactive - context['header_subtitle'] = self.request.entity.name - context['header_subtitle_icon'] = 'gravity-ui:hierarchy' - context['page_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List' - context['header_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List' + context["inactive"] = self.inactive + context["header_subtitle"] = self.request.entity.name + context["header_subtitle_icon"] = "gravity-ui:hierarchy" + context["page_title"] = ( + "Inactive Chart of Account List" + if self.inactive + else "Chart of Accounts List" + ) + context["header_title"] = ( + "Inactive Chart of Account List" + if self.inactive + else "Chart of Accounts List" + ) return context + class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, CreateView): - template_name = 'chart_of_accounts/coa_create.html' + template_name = "chart_of_accounts/coa_create.html" extra_context = { - 'header_title': _('Create Chart of Accounts'), - 'page_title': _('Create Chart of Account'), + "header_title": _("Create Chart of Accounts"), + "page_title": _("Create Chart of Account"), } def get_initial(self): return { - 'entity': self.request.entity, + "entity": self.request.entity, } def get_form(self, form_class=None): return ChartOfAccountsModelCreateForm( - entity_model=self.request.entity, - **self.get_form_kwargs() + entity_model=self.request.entity, **self.get_form_kwargs() ) def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=None, **kwargs) - context['header_subtitle'] = f'New Chart of Accounts: {self.request.entity.name}' - context['header_subtitle_icon'] = 'gravity-ui:hierarchy' + context["header_subtitle"] = ( + f"New Chart of Accounts: {self.request.entity.name}" + ) + context["header_subtitle_icon"] = "gravity-ui:hierarchy" return context def get_success_url(self): - return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, - 'entity_slug': self.request.entity.slug}) + return reverse( + "coa-list", + kwargs={ + "dealer_slug": self.request.dealer.slug, + "entity_slug": self.request.entity.slug, + }, + ) class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView): - context_object_name = 'coa_model' - slug_url_kwarg = 'coa_slug' - template_name = 'chart_of_accounts/coa_update.html' + context_object_name = "coa_model" + slug_url_kwarg = "coa_slug" + template_name = "chart_of_accounts/coa_update.html" form_class = ChartOfAccountsModelUpdateForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) chart_of_accounts_model: ChartOfAccountModel = self.object - context['page_title'] = f'Update Chart of Account {chart_of_accounts_model.name}' - context['header_title'] = f'Update Chart of Account {chart_of_accounts_model.name}' + context["page_title"] = ( + f"Update Chart of Account {chart_of_accounts_model.name}" + ) + context["header_title"] = ( + f"Update Chart of Account {chart_of_accounts_model.name}" + ) return context def get_success_url(self): - return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, - 'entity_slug': self.request.entity.slug}) + return reverse( + "coa-list", + kwargs={ + "dealer_slug": self.request.dealer.slug, + "entity_slug": self.request.entity.slug, + }, + ) -class CharOfAccountModelActionView(ChartOfAccountModelModelBaseViewMixIn, - RedirectView, - SingleObjectMixin): - http_method_names = ['get'] - slug_url_kwarg = 'coa_slug' +class CharOfAccountModelActionView( + ChartOfAccountModelModelBaseViewMixIn, RedirectView, SingleObjectMixin +): + http_method_names = ["get"] + slug_url_kwarg = "coa_slug" action_name = None commit = True def get(self, request, *args, **kwargs): - kwargs['user_model'] = self.request.user + kwargs["user_model"] = self.request.user if not self.action_name: - raise ImproperlyConfigured('View attribute action_name is required.') - response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs) + raise ImproperlyConfigured("View attribute action_name is required.") + response = super(CharOfAccountModelActionView, self).get( + request, *args, **kwargs + ) coa_model: ChartOfAccountModel = self.get_object() try: getattr(coa_model, self.action_name)(commit=self.commit, **kwargs) - messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success', - message=_('Successfully updated {} Default Chart of Account to '.format( - request.entity.name) + - '{}'.format(coa_model.name))) + messages.add_message( + request, + level=messages.SUCCESS, + extra_tags="is-success", + message=_( + "Successfully updated {} Default Chart of Account to ".format( + request.entity.name + ) + + "{}".format(coa_model.name) + ), + ) except ValidationError as e: - messages.add_message(request, - message=e.message, - level=messages.ERROR, - extra_tags='is-danger') + messages.add_message( + request, message=e.message, level=messages.ERROR, extra_tags="is-danger" + ) return response diff --git a/inventory/signals.py b/inventory/signals.py index 3a1ef0ef..19f55a60 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -28,6 +28,7 @@ from plans.models import UserPlan from plans.signals import order_completed, activate_user_plan from inventory.tasks import send_email from django.conf import settings + # logging import logging @@ -177,7 +178,11 @@ def create_ledger_entity(sender, instance, created, **kwargs): entity.create_uom(name=u[1], unit_abbr=u[0]) # Create COA accounts, background task - async_task(func="inventory.tasks.create_coa_accounts",dealer=instance,hook="inventory.hooks.check_create_coa_accounts") + async_task( + func="inventory.tasks.create_coa_accounts", + 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)) # create_settings(instance.pk) @@ -273,14 +278,14 @@ def create_item_model(sender, instance, created, **kwargs): else: instance.item_model.default_amount = instance.marked_price - # inventory = entity.create_item_inventory( - # name=instance.vin, - # uom_model=uom, - # item_type=ItemModel.ITEM_TYPE_LUMP_SUM - # ) - # inventory.additional_info = {} - # inventory.additional_info.update({"car_info": instance.to_dict()}) - # inventory.save() + # inventory = entity.create_item_inventory( + # name=instance.vin, + # uom_model=uom, + # item_type=ItemModel.ITEM_TYPE_LUMP_SUM + # ) + # inventory.additional_info = {} + # inventory.additional_info.update({"car_info": instance.to_dict()}) + # inventory.save() # else: # instance.item_model.additional_info.update({"car_info": instance.to_dict()}) # instance.item_model.save() @@ -1039,7 +1044,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs): po_number=instance.po_number, url=reverse( "purchase_order_detail", - kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk}, + kwargs={ + "dealer_slug": dealer.slug, + "entity_slug": instance.entity.slug, + "pk": instance.pk, + }, ), ), ) @@ -1086,7 +1095,10 @@ def sale_order_created_notification(sender, instance, created, **kwargs): estimate_number=instance.estimate.estimate_number, url=reverse( "estimate_detail", - kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk}, + kwargs={ + "dealer_slug": instance.dealer.slug, + "pk": instance.estimate.pk, + }, ), ), ) @@ -1131,7 +1143,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs): url=reverse( "estimate_detail", kwargs={"dealer_slug": dealer.slug, "pk": instance.pk}, - ) + ), ), ) @@ -1188,7 +1200,11 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs): bill_number=instance.bill_number, url=reverse( "bill-update", - kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk}, + kwargs={ + "dealer_slug": dealer.slug, + "entity_slug": dealer.entity.slug, + "bill_pk": instance.pk, + }, ), ), ) @@ -1224,7 +1240,6 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs): # ) - @receiver(post_save, sender=models.Ticket) def send_ticket_notification(sender, instance, created, **kwargs): if created: @@ -1249,20 +1264,23 @@ def send_ticket_notification(sender, instance, created, **kwargs): ) else: models.Notification.objects.create( - user=instance.dealer.user, - message=_( - """ + user=instance.dealer.user, + message=_( + """ Support Ticket #{ticket_number} has been updated. View. """ - ).format( - ticket_number=instance.pk, - url=reverse( - "ticket_detail", - kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk}, - ), + ).format( + ticket_number=instance.pk, + url=reverse( + "ticket_detail", + kwargs={ + "dealer_slug": instance.dealer.slug, + "ticket_id": instance.pk, + }, ), - ) + ), + ) @receiver(post_save, sender=models.CarColors) @@ -1273,30 +1291,31 @@ def handle_car_image(sender, instance, created, **kwargs): try: # Create or get car image record car = instance.car - car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash}) + car_image, created = models.CarImage.objects.get_or_create( + car=car, defaults={"image_hash": car.get_hash} + ) # Check for existing image with same hash - existing = models.CarImage.objects.filter( - image_hash=car_image.image_hash, - image__isnull=False - ).exclude(car=car).first() + existing = ( + models.CarImage.objects.filter( + image_hash=car_image.image_hash, image__isnull=False + ) + .exclude(car=car) + .first() + ) if existing: # Copy existing image - car_image.image.save( - existing.image.name, - existing.image.file, - save=True - ) + car_image.image.save(existing.image.name, existing.image.file, save=True) logger.info(f"Reused image for car {car.vin}") else: # Schedule async generation async_task( - 'inventory.tasks.generate_car_image_task', + "inventory.tasks.generate_car_image_task", car_image.id, - task_name=f"generate_car_image_{car.vin}" + task_name=f"generate_car_image_{car.vin}", ) logger.info(f"Scheduled image generation for car {car.vin}") except Exception as e: - logger.error(f"Error handling car image for {car.vin}: {e}") \ No newline at end of file + logger.error(f"Error handling car image for {car.vin}: {e}") diff --git a/inventory/tasks.py b/inventory/tasks.py index e0a52c12..31df3a19 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -17,11 +17,19 @@ 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 .utils import get_accounts_data, create_account from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User, Group, Permission -from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage +from inventory.models import ( + DealerSettings, + Dealer, + Schedule, + Notification, + CarReservation, + CarStatusChoices, + CarImage, +) logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -52,6 +60,7 @@ def create_settings(pk): .first(), ) + def create_coa_accounts(**kwargs): logger.info("creating all accounts are created") instance = kwargs.get("dealer") @@ -62,9 +71,6 @@ def create_coa_accounts(**kwargs): create_account(entity, coa, account_data) - - - # def create_coa_accounts1(pk): # with transaction.atomic(): # instance = Dealer.objects.select_for_update().get(pk=pk) @@ -800,8 +806,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr # transaction.on_commit(run) - - def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire): """Send bilingual email reminder using Django-Q""" try: @@ -809,41 +813,49 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire plan = Plan.objects.get(id=plan_id) # Determine user language preference - user_language = getattr(user, 'language', settings.LANGUAGE_CODE) + user_language = getattr(user, "language", settings.LANGUAGE_CODE) activate(user_language) # Context data context = { - 'user': user, - 'plan': plan, - 'expiration_date': expiration_date, - 'days_until_expire': days_until_expire, - 'SITE_NAME': settings.SITE_NAME, - 'RENEWAL_URL': "url" ,#settings.RENEWAL_URL, - 'direction': 'rtl' if user_language.startswith('ar') else 'ltr' + "user": user, + "plan": plan, + "expiration_date": expiration_date, + "days_until_expire": days_until_expire, + "SITE_NAME": settings.SITE_NAME, + "RENEWAL_URL": "url", # settings.RENEWAL_URL, + "direction": "rtl" if user_language.startswith("ar") else "ltr", } # Subject with translation - subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days" + subject_en = ( + f"Your {plan.name} subscription expires in {days_until_expire} days" + ) subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام" # Render templates - text_content = render_to_string([ - f'emails/expiration_reminder_{user_language}.txt', - 'emails/expiration_reminder.txt' - ], context) + text_content = render_to_string( + [ + f"emails/expiration_reminder_{user_language}.txt", + "emails/expiration_reminder.txt", + ], + context, + ) - html_content = render_to_string([ - f'emails/expiration_reminder_{user_language}.html', - 'emails/expiration_reminder.html' - ], context) + html_content = render_to_string( + [ + f"emails/expiration_reminder_{user_language}.html", + "emails/expiration_reminder.html", + ], + context, + ) # Create email email = EmailMultiAlternatives( - subject=subject_ar if user_language.startswith('ar') else subject_en, + subject=subject_ar if user_language.startswith("ar") else subject_en, body=text_content, from_email=settings.DEFAULT_FROM_EMAIL, - to=[user.email] + to=[user.email], ) email.attach_alternative(html_content, "text/html") email.send() @@ -853,6 +865,7 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire logger.error(f"Email failed: {str(e)}") raise + def handle_email_result(task): """Callback for email results""" if task.success: @@ -861,7 +874,6 @@ def handle_email_result(task): logger.error(f"Email task failed: {task.result}") - def send_schedule_reminder_email(schedule_id): """ Sends an email reminder for a specific schedule. @@ -871,46 +883,67 @@ def send_schedule_reminder_email(schedule_id): schedule = Schedule.objects.get(pk=schedule_id) # Ensure the user has an email and the schedule is not completed/canceled - if not schedule.scheduled_by.email or schedule.status in ["completed", "canceled"]: - logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.") + if not schedule.scheduled_by.email or schedule.status in [ + "completed", + "canceled", + ]: + logger.error( + f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}." + ) return user_email = schedule.scheduled_by.email Notification.objects.create( - user=schedule.scheduled_by, - message=_( - """ + user=schedule.scheduled_by, + message=_( + """ Reminder: You have an appointment scheduled for {scheduled_type} After 15 minutes View. """ - ).format(scheduled_type=schedule.scheduled_type, url=reverse("schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug})),) + ).format( + scheduled_type=schedule.scheduled_type, + url=reverse( + "schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug} + ), + ), + ) # Prepare context for email templates context = { - 'schedule_purpose': schedule.purpose, - 'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone - 'schedule_type': schedule.scheduled_type, - 'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A', - 'notes': schedule.notes, - 'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email, + "schedule_purpose": schedule.purpose, + "scheduled_at": schedule.scheduled_at.astimezone( + timezone.get_current_timezone() + ).strftime("%Y-%m-%d %H:%M %Z"), # Format with timezone + "schedule_type": schedule.scheduled_type, + "customer_name": schedule.customer.customer_name + if schedule.customer + else "N/A", + "notes": schedule.notes, + "user_name": schedule.scheduled_by.get_full_name() + or schedule.scheduled_by.email, } # Render email content from templates - html_message = render_to_string('emails/schedule_reminder.html', context) - plain_message = render_to_string('emails/schedule_reminder.txt', context) + html_message = render_to_string("emails/schedule_reminder.html", context) + plain_message = render_to_string("emails/schedule_reminder.txt", context) send_mail( - f'Reminder: Your Upcoming Schedule - {schedule.purpose}', + f"Reminder: Your Upcoming Schedule - {schedule.purpose}", plain_message, settings.DEFAULT_FROM_EMAIL, [user_email], html_message=html_message, ) - logger.info(f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}") + logger.info( + f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}" + ) except Schedule.DoesNotExist: - logger.info(f"Schedule with ID {schedule_id} does not exist. Cannot send reminder.") + logger.info( + f"Schedule with ID {schedule_id} does not exist. Cannot send reminder." + ) except Exception as e: logger.info(f"Error sending reminder email for Schedule ID {schedule_id}: {e}") + # Optional: A hook function to log the status of the email task (add to your_app/tasks.py) def log_email_status(task): """ @@ -918,9 +951,14 @@ def log_email_status(task): It logs whether the task was successful or not. """ if task.success: - logger.info(f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}") + logger.info( + f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}" + ) else: - logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}") + logger.error( + f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}" + ) + def remove_reservation_by_id(reservation_id): try: @@ -931,8 +969,9 @@ def remove_reservation_by_id(reservation_id): except Exception as e: logger.error(f"Error removing reservation with ID {reservation_id}: {e}") + def test_task(**kwargs): - print("TASK : ",kwargs.get("dealer")) + print("TASK : ", kwargs.get("dealer")) def generate_car_image_task(car_image_id): @@ -940,22 +979,25 @@ def generate_car_image_task(car_image_id): Simple async task to generate car image """ from inventory.utils import generate_car_image_simple + try: car_image = CarImage.objects.get(id=car_image_id) result = generate_car_image_simple(car_image) return { - 'success': result.get('success', False), - 'car_image_id': car_image_id, - 'error': result.get('error'), - 'message': 'Image generated' if result.get('success') else 'Generation failed' + "success": result.get("success", False), + "car_image_id": car_image_id, + "error": result.get("error"), + "message": "Image generated" + if result.get("success") + else "Generation failed", } except CarImage.DoesNotExist: error_msg = f"CarImage with id {car_image_id} not found" logger.error(error_msg) - return {'success': False, 'error': error_msg} + return {"success": False, "error": error_msg} except Exception as e: error_msg = f"Unexpected error: {e}" logger.error(error_msg) - return {'success': False, 'error': error_msg} \ No newline at end of file + return {"success": False, "error": error_msg} diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 1da1709d..72f5c9f8 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -13,9 +13,9 @@ from django.db.models import Case, Value, When, IntegerField register = template.Library() + @register.filter def get_percentage(value, total): - try: value = int(value) total = int(total) @@ -25,6 +25,7 @@ def get_percentage(value, total): except (ValueError, TypeError): return 0 + @register.filter(name="percentage") def percentage(value): if value is not None: @@ -686,11 +687,12 @@ def count_checked(permissions, group_permission_ids): # """Count how many permissions are checked from the allowed list""" # return sum(1 for perm in permissions if perm.id in group_permission_ids) -@register.inclusion_tag('sales/tags/invoice_item_formset.html', takes_context=True) + +@register.inclusion_tag("sales/tags/invoice_item_formset.html", takes_context=True) def invoice_item_formset_table(context, itemtxs_formset): return { - 'entity_slug': context['view'].kwargs['entity_slug'], - 'invoice_model': context['invoice'], - 'total_amount__sum': context['total_amount__sum'], - 'itemtxs_formset': itemtxs_formset, + "entity_slug": context["view"].kwargs["entity_slug"], + "invoice_model": context["invoice"], + "total_amount__sum": context["total_amount__sum"], + "itemtxs_formset": itemtxs_formset, } diff --git a/inventory/urls.py b/inventory/urls.py index 028098d7..72e48500 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -40,13 +40,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, @@ -777,7 +782,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, @@ -934,7 +943,6 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), - # Expanese path( "/items/expeneses/", @@ -1093,32 +1101,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( @@ -1294,40 +1317,74 @@ 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//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//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'), - ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/utils.py b/inventory/utils.py index cabfb161..ede69b63 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -73,15 +73,12 @@ def get_jwt_token(): try: response = requests.post(url, headers=headers, json=data) response.raise_for_status() - #logging for success + # logging for success logger.info("Successfully fetched JWT token.") return response.text except requests.exceptions.RequestException as e: - #logging for error - logger.error( - f"HTTP error fetching JWT token from {url}: ", - exc_info=True - ) + # logging for error + logger.error(f"HTTP error fetching JWT token from {url}: ", exc_info=True) print(f"Error obtaining JWT token: {e}") return None @@ -169,7 +166,7 @@ def send_email(from_, to_, subject, message): message = message from_email = from_ recipient_list = [to_] - async_task(send_mail,subject, message, from_email, recipient_list) + async_task(send_mail, subject, message, from_email, recipient_list) def get_user_type(request): @@ -236,10 +233,10 @@ def reserve_car(car, request): ) car.status = models.CarStatusChoices.RESERVED car.save() - # --- Logging for Success --- + # --- Logging for Success --- DjangoQSchedule.objects.create( name=f"remove_reservation_for_car_with_vin_{car.vin}", - func='inventory.tasks.remove_reservation_by_id', + func="inventory.tasks.remove_reservation_by_id", args=reservation.pk, schedule_type=DjangoQSchedule.ONCE, next_run=reserved_until, @@ -257,7 +254,7 @@ def reserve_car(car, request): f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') " f"for user {request.user} . " f"Error: {e}", - exc_info=True + exc_info=True, ) messages.error(request, f"Error reserving car: {e}") @@ -1038,22 +1035,25 @@ class CarFinanceCalculator1: self.item_transactions = self._get_item_transactions() # self.additional_services = self._get_additional_services() - def _get_vat_rate(self): - vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first() + vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() if not vat: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate def _get_additional_services(self): - return [x for item in self.item_transactions - for x in item.item_model.car.additional_services - ] + return [ + x + for item in self.item_transactions + for x in item.item_model.car.additional_services + ] + def _get_item_transactions(self): return self.model.get_itemtxs_data()[0].all() def get_items(self): return self._get_item_transactions() + @staticmethod def _get_quantity(item): return item.ce_quantity or item.quantity @@ -1068,17 +1068,17 @@ class CarFinanceCalculator1: quantity = self._get_quantity(item) car = item.item_model.car unit_price = Decimal(car.marked_price) - discount = self.extra_info.data.get("discount",0) + discount = self.extra_info.data.get("discount", 0) sell_price = unit_price - Decimal(discount) return { "item_number": item.item_model.item_number, - "vin": car.vin, #car_info.get("vin"), - "make": car.id_car_make ,#car_info.get("make"), - "model": car.id_car_model ,#car_info.get("model"), - "year": car.year ,# car_info.get("year"), - "logo": car.logo, # getattr(car.id_car_make, "logo", ""), - "trim": car.id_car_trim ,# car_info.get("trim"), - "mileage": car.mileage ,# car_info.get("mileage"), + "vin": car.vin, # car_info.get("vin"), + "make": car.id_car_make, # car_info.get("make"), + "model": car.id_car_model, # car_info.get("model"), + "year": car.year, # car_info.get("year"), + "logo": car.logo, # getattr(car.id_car_make, "logo", ""), + "trim": car.id_car_trim, # car_info.get("trim"), + "mileage": car.mileage, # car_info.get("mileage"), "cost_price": car.cost_price, "selling_price": car.selling_price, "marked_price": car.marked_price, @@ -1091,21 +1091,23 @@ class CarFinanceCalculator1: "total_discount": discount, "final_price": sell_price + (sell_price * self.vat_rate), "total_additionals": car.total_additional_services, - "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, - "additional_services": car.additional_services,# self._get_nested_value( - #item, self.ADDITIONAL_SERVICES_KEY - #), + "grand_total": sell_price + + (sell_price * self.vat_rate) + + car.total_additional_services, + "additional_services": car.additional_services, # self._get_nested_value( + # item, self.ADDITIONAL_SERVICES_KEY + # ), } def calculate_totals(self): total_price = sum( - Decimal(item.item_model.car.marked_price) - for item in self.item_transactions + Decimal(item.item_model.car.marked_price) for item in self.item_transactions ) total_additionals = sum( - Decimal(item.price_) for item in self._get_additional_services()) + Decimal(item.price_) for item in self._get_additional_services() + ) - total_discount = self.extra_info.data.get("discount",0) + total_discount = self.extra_info.data.get("discount", 0) total_price_discounted = total_price if total_discount: total_price_discounted = total_price - Decimal(total_discount) @@ -1113,13 +1115,15 @@ class CarFinanceCalculator1: total_vat_amount = total_price_discounted * self.vat_rate return { - "total_price_discounted":total_price_discounted, - "total_price_before_discount":total_price, + "total_price_discounted": total_price_discounted, + "total_price_before_discount": total_price, "total_price": total_price_discounted, "total_vat_amount": total_vat_amount, "total_discount": Decimal(total_discount), "total_additionals": total_additionals, - "grand_total":total_price_discounted + total_vat_amount + total_additionals, + "grand_total": total_price_discounted + + total_vat_amount + + total_additionals, } def get_finance_data(self): @@ -1131,7 +1135,9 @@ class CarFinanceCalculator1: ), "total_price": round(totals["total_price"], 2), "total_price_discounted": round(totals["total_price_discounted"], 2), - "total_price_before_discount": round(totals["total_price_before_discount"], 2), + "total_price_before_discount": round( + totals["total_price_before_discount"], 2 + ), "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2), "total_discount": round(totals["total_discount"], 2), @@ -1140,6 +1146,8 @@ class CarFinanceCalculator1: "additionals": self._get_additional_services(), "vat": round(self.vat_rate, 2), } + + class CarFinanceCalculator: """ Class responsible for calculating car financing details. @@ -1185,22 +1193,25 @@ class CarFinanceCalculator: self.item_transactions = self._get_item_transactions() # self.additional_services = self._get_additional_services() - def _get_vat_rate(self): - vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first() + vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() if not vat: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate def _get_additional_services(self): - return [x for item in self.item_transactions - for x in item.item_model.car.additional_services - ] + return [ + x + for item in self.item_transactions + for x in item.item_model.car.additional_services + ] + def _get_item_transactions(self): return self.model.get_itemtxs_data()[0].all() def get_items(self): return self._get_item_transactions() + @staticmethod def _get_quantity(item): return item.ce_quantity or item.quantity @@ -1215,17 +1226,17 @@ class CarFinanceCalculator: quantity = self._get_quantity(item) car = item.item_model.car unit_price = Decimal(car.marked_price) - discount = self.extra_info.data.get("discount",0) + discount = self.extra_info.data.get("discount", 0) sell_price = unit_price - Decimal(discount) return { "item_number": item.item_model.item_number, - "vin": car.vin, #car_info.get("vin"), - "make": car.id_car_make ,#car_info.get("make"), - "model": car.id_car_model ,#car_info.get("model"), - "year": car.year ,# car_info.get("year"), - "logo": car.logo, # getattr(car.id_car_make, "logo", ""), - "trim": car.id_car_trim ,# car_info.get("trim"), - "mileage": car.mileage ,# car_info.get("mileage"), + "vin": car.vin, # car_info.get("vin"), + "make": car.id_car_make, # car_info.get("make"), + "model": car.id_car_model, # car_info.get("model"), + "year": car.year, # car_info.get("year"), + "logo": car.logo, # getattr(car.id_car_make, "logo", ""), + "trim": car.id_car_trim, # car_info.get("trim"), + "mileage": car.mileage, # car_info.get("mileage"), "cost_price": car.cost_price, "selling_price": car.selling_price, "marked_price": car.marked_price, @@ -1238,21 +1249,23 @@ class CarFinanceCalculator: "total_discount": discount, "final_price": sell_price + (sell_price * self.vat_rate), "total_additionals": car.total_additional_services, - "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, - "additional_services": car.additional_services,# self._get_nested_value( - #item, self.ADDITIONAL_SERVICES_KEY - #), + "grand_total": sell_price + + (sell_price * self.vat_rate) + + car.total_additional_services, + "additional_services": car.additional_services, # self._get_nested_value( + # item, self.ADDITIONAL_SERVICES_KEY + # ), } def calculate_totals(self): total_price = sum( - Decimal(item.item_model.car.marked_price) - for item in self.item_transactions + Decimal(item.item_model.car.marked_price) for item in self.item_transactions ) total_additionals = sum( - Decimal(item.price_) for item in self._get_additional_services()) + Decimal(item.price_) for item in self._get_additional_services() + ) - total_discount = self.extra_info.data.get("discount",0) + total_discount = self.extra_info.data.get("discount", 0) total_price_discounted = total_price if total_discount: total_price_discounted = total_price - Decimal(total_discount) @@ -1260,13 +1273,15 @@ class CarFinanceCalculator: total_vat_amount = total_price_discounted * self.vat_rate return { - "total_price_discounted":total_price_discounted, - "total_price_before_discount":total_price, + "total_price_discounted": total_price_discounted, + "total_price_before_discount": total_price, "total_price": total_price_discounted, "total_vat_amount": total_vat_amount, "total_discount": Decimal(total_discount), "total_additionals": total_additionals, - "grand_total":total_price_discounted + total_vat_amount + total_additionals, + "grand_total": total_price_discounted + + total_vat_amount + + total_additionals, } def get_finance_data(self): @@ -1278,7 +1293,9 @@ class CarFinanceCalculator: ), "total_price": round(totals["total_price"], 2), "total_price_discounted": round(totals["total_price_discounted"], 2), - "total_price_before_discount": round(totals["total_price_before_discount"], 2), + "total_price_before_discount": round( + totals["total_price_before_discount"], 2 + ), "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2), "total_discount": round(totals["total_discount"], 2), @@ -1288,58 +1305,60 @@ class CarFinanceCalculator: "vat": round(self.vat_rate, 2), } -def get_finance_data(estimate,dealer): - vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() - item = estimate.get_itemtxs_data()[0].first() - car = item.item_model.car - if isinstance(estimate,InvoiceModel) and hasattr(estimate, "ce_model"): - estimate = estimate.ce_model - extra_info = models.ExtraInfo.objects.get( - dealer=dealer, - content_type=ContentType.objects.get_for_model(EstimateModel), - object_id=estimate.pk, - ) - discount = extra_info.data.get("discount", 0) - discount = Decimal(discount) +def get_finance_data(estimate, dealer): + vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() + item = estimate.get_itemtxs_data()[0].first() + car = item.item_model.car + if isinstance(estimate, InvoiceModel) and hasattr(estimate, "ce_model"): + estimate = estimate.ce_model - additional_services = car.get_additional_services() - discounted_price=(Decimal(car.marked_price) - discount) - vat_amount = discounted_price * vat.rate - total_services_vat=sum([x[1] for x in additional_services.get("services")]) - total_vat=vat_amount+total_services_vat - return { - "car": car, - "discounted_price": discounted_price or 0, - "price_before_discount": car.marked_price, - "vat_amount": vat_amount, - "vat_rate": vat.rate, - "discount_amount": discount, - "additional_services": additional_services, - "final_price": discounted_price + vat_amount, - "total_services_vat":total_services_vat, - "total_vat":total_vat, - "grand_total": discounted_price + total_vat + additional_services.get("total") - } + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk, + ) + 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_vat = sum([x[1] for x in additional_services.get("services")]) + total_vat = vat_amount + total_services_vat + return { + "car": car, + "discounted_price": discounted_price or 0, + "price_before_discount": car.marked_price, + "vat_amount": vat_amount, + "vat_rate": vat.rate, + "discount_amount": discount, + "additional_services": additional_services, + "final_price": discounted_price + vat_amount, + "total_services_vat": total_services_vat, + "total_vat": total_vat, + "grand_total": discounted_price + total_vat + additional_services.get("total"), + } + + # totals = self.calculate_totals() + # return { + # "car": [self._get_car_data(item) for item in self.item_transactions], + # "quantity": sum( + # self._get_quantity(item) for item in self.item_transactions + # ), + # "total_price": round(totals["total_price"], 2), + # "total_price_discounted": round(totals["total_price_discounted"], 2), + # "total_price_before_discount": round(totals["total_price_before_discount"], 2), + # "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), + # "total_vat_amount": round(totals["total_vat_amount"], 2), + # "total_discount": round(totals["total_discount"], 2), + # "total_additionals": round(totals["total_additionals"], 2), + # "grand_total": round(totals["grand_total"], 2), + # "additionals": self._get_additional_services(), + # "vat": round(self.vat_rate, 2), + # } - # totals = self.calculate_totals() - # return { - # "car": [self._get_car_data(item) for item in self.item_transactions], - # "quantity": sum( - # self._get_quantity(item) for item in self.item_transactions - # ), - # "total_price": round(totals["total_price"], 2), - # "total_price_discounted": round(totals["total_price_discounted"], 2), - # "total_price_before_discount": round(totals["total_price_before_discount"], 2), - # "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2), - # "total_vat_amount": round(totals["total_vat_amount"], 2), - # "total_discount": round(totals["total_discount"], 2), - # "total_additionals": round(totals["total_additionals"], 2), - # "grand_total": round(totals["grand_total"], 2), - # "additionals": self._get_additional_services(), - # "vat": round(self.vat_rate, 2), - # } # class CarFinanceCalculator: # """ # Class responsible for calculating car financing details. @@ -1554,7 +1573,6 @@ def get_local_name(self): return getattr(self, "name", None) - @transaction.atomic def set_invoice_payment(dealer, entity, invoice, amount, payment_method): """ @@ -1566,6 +1584,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method): _post_sale_and_cogs(invoice, dealer) + def _post_sale_and_cogs(invoice, dealer): """ For every car line on the invoice: @@ -1574,15 +1593,39 @@ def _post_sale_and_cogs(invoice, dealer): """ entity = invoice.ledger.entity # calc = CarFinanceCalculator(invoice) - data = get_finance_data(invoice,dealer) + data = get_finance_data(invoice, dealer) car = data.get("car") - cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first() - ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first() - vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() - car_rev = entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() - add_rev = entity.get_default_coa_accounts().filter(code="4020").first() - cogs_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() - inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() + cash_acc = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.ASSET_CA_CASH) + .first() + ) + ar_acc = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES) + .first() + ) + vat_acc = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE) + .first() + ) + car_rev = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.INCOME_OPERATIONAL) + .first() + ) + add_rev = entity.get_default_coa_accounts().filter(code="4020").first() + cogs_acc = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.COGS) + .first() + ) + inv_acc = ( + entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.ASSET_CA_INVENTORY) + .first() + ) # for car_data in data['cars']: # car = invoice.get_itemtxs_data()[0].filter( @@ -1590,12 +1633,12 @@ def _post_sale_and_cogs(invoice, dealer): # ).first().item_model.car # qty = Decimal(car_data['quantity']) - net_car_price = Decimal(data['discounted_price']) - net_additionals_price = Decimal(data['additional_services']['total']) - vat_amount = Decimal(data['vat_amount']) - grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount - cost_total = Decimal(car.cost_price) - discount_amount =Decimal(data['discount_amount']) + net_car_price = Decimal(data["discounted_price"]) + net_additionals_price = Decimal(data["additional_services"]["total"]) + vat_amount = Decimal(data["vat_amount"]) + grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount + cost_total = Decimal(car.cost_price) + discount_amount = Decimal(data["discount_amount"]) # ------------------------------------------------------------------ # 2A. Journal: Cash / A-R / VAT / Sales @@ -1606,15 +1649,15 @@ def _post_sale_and_cogs(invoice, dealer): description=f"Sale {car.vin}", origin=f"Invoice {invoice.invoice_number}", locked=False, - posted=False + posted=False, ) # Dr Cash (what the customer paid) TransactionModel.objects.create( journal_entry=je_sale, account=cash_acc, amount=grand_total, - tx_type='debit', - description='Debit to Cash on Hand' + tx_type="debit", + description="Debit to Cash on Hand", ) # # Cr A/R (clear the receivable) @@ -1630,8 +1673,8 @@ def _post_sale_and_cogs(invoice, dealer): journal_entry=je_sale, account=vat_acc, amount=vat_amount, - tx_type='credit', - description="Credit to Tax Payable" + tx_type="credit", + description="Credit to Tax Payable", ) # Cr Sales – Car @@ -1639,8 +1682,8 @@ def _post_sale_and_cogs(invoice, dealer): journal_entry=je_sale, account=car_rev, amount=net_car_price, - tx_type='credit', - description=" Credit to Car Sales" + tx_type="credit", + description=" Credit to Car Sales", ) if car.get_additional_services_amount > 0: @@ -1649,16 +1692,15 @@ def _post_sale_and_cogs(invoice, dealer): journal_entry=je_sale, account=add_rev, amount=car.get_additional_services_amount, - tx_type='credit', - description="Credit to After-Sales Services" + tx_type="credit", + description="Credit to After-Sales Services", ) TransactionModel.objects.create( journal_entry=je_sale, - account=vat_acc, amount=car.get_additional_services_vat, - tx_type='credit', - description="Credit to Tax Payable (Additional Services)" + tx_type="credit", + description="Credit to Tax Payable (Additional Services)", ) # ------------------------------------------------------------------ @@ -1669,7 +1711,7 @@ def _post_sale_and_cogs(invoice, dealer): description=f"COGS {car.vin}", origin=f"Invoice {invoice.invoice_number}", locked=False, - posted=False + posted=False, ) # Dr COGS @@ -1677,15 +1719,12 @@ def _post_sale_and_cogs(invoice, dealer): journal_entry=je_cogs, account=cogs_acc, amount=cost_total, - tx_type='debit', + tx_type="debit", ) # Cr Inventory TransactionModel.objects.create( - journal_entry=je_cogs, - account=inv_acc, - amount=cost_total, - tx_type='credit' + journal_entry=je_cogs, account=inv_acc, amount=cost_total, tx_type="credit" ) # ------------------------------------------------------------------ # 2C. Update car state flags inside the same transaction @@ -1693,10 +1732,12 @@ def _post_sale_and_cogs(invoice, dealer): entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False) # car.item_model.for_inventory = False # car.item_model.save(update_fields=['for_inventory']) - car.discount_amount=discount_amount + car.discount_amount = discount_amount car.selling_price = grand_total # car.is_sold = True car.save() + + # def handle_account_process(invoice, amount, finance_data): # """ # Processes accounting transactions based on an invoice, financial data, @@ -1787,29 +1828,29 @@ def _post_sale_and_cogs(invoice, dealer): # car.finances.save() # car.item_model.save() - # TransactionModel.objects.create( - # journal_entry=journal, - # account=additional_services_account, # Debit Additional Services - # amount=Decimal(car.finances.total_additionals), - # tx_type="debit", - # description="Additional Services", - # ) +# TransactionModel.objects.create( +# journal_entry=journal, +# account=additional_services_account, # Debit Additional Services +# amount=Decimal(car.finances.total_additionals), +# tx_type="debit", +# description="Additional Services", +# ) - # TransactionModel.objects.create( - # journal_entry=journal, - # account=inventory_account, # Credit Inventory account - # amount=Decimal(finance_data.get("grand_total")), - # tx_type="credit", - # description="Account Adjustment", - # ) +# TransactionModel.objects.create( +# journal_entry=journal, +# account=inventory_account, # Credit Inventory account +# amount=Decimal(finance_data.get("grand_total")), +# tx_type="credit", +# description="Account Adjustment", +# ) - # TransactionModel.objects.create( - # journal_entry=journal, - # account=vat_payable_account, # Credit VAT Payable - # amount=finance_data.get("total_vat_amount"), - # tx_type="credit", - # description="VAT Payable on Invoice", - # ) +# TransactionModel.objects.create( +# journal_entry=journal, +# account=vat_payable_account, # Credit VAT Payable +# amount=finance_data.get("total_vat_amount"), +# tx_type="credit", +# description="VAT Payable on Invoice", +# ) def create_make_accounts(dealer): @@ -1857,6 +1898,7 @@ def create_make_accounts(dealer): active=True, ) + def handle_payment(request, order): url = "https://api.moyasar.com/v1/payments" callback_url = request.build_absolute_uri( @@ -1943,7 +1985,6 @@ def handle_payment(request, order): # return user.dealer.quota - def get_accounts_data(): return [ # Current Assets (must start with 1) @@ -2339,6 +2380,7 @@ def get_accounts_data(): }, ] + def create_account(entity, coa, account_data): try: account = entity.create_account( @@ -2371,17 +2413,16 @@ def get_or_generate_car_image(car): return car_image.image.url # Check for existing image with same hash - existing = models.CarImage.objects.filter( - image_hash=car_image.image_hash, - image__isnull=False - ).exclude(car=car).first() + existing = ( + models.CarImage.objects.filter( + image_hash=car_image.image_hash, image__isnull=False + ) + .exclude(car=car) + .first() + ) if existing: - car_image.image.save( - existing.image.name, - existing.image.file, - save=True - ) + car_image.image.save(existing.image.name, existing.image.file, save=True) return car_image.image.url # If no image exists and not already generating, schedule generation @@ -2394,6 +2435,7 @@ def get_or_generate_car_image(car): logger.error(f"Error getting/generating car image: {e}") return None + def force_regenerate_car_image(car): """ Force regeneration of car image (useful for admin actions) @@ -2414,6 +2456,7 @@ def force_regenerate_car_image(car): logger.error(f"Error forcing image regeneration: {e}") return False + class CarImageAPIClient: """Simple client to handle authenticated requests to the car image API""" @@ -2436,7 +2479,7 @@ class CarImageAPIClient: response.raise_for_status() # Get CSRF token from cookies - self.csrf_token = self.session.cookies.get('csrftoken') + self.csrf_token = self.session.cookies.get("csrftoken") if not self.csrf_token: raise Exception("CSRF token not found in cookies") @@ -2444,16 +2487,17 @@ class CarImageAPIClient: login_data = { "username": self.USERNAME, "password": self.PASSWORD, - "csrfmiddlewaretoken": self.csrf_token + "csrfmiddlewaretoken": self.csrf_token, } login_response = self.session.post( - f"{self.BASE_URL}/login", - data=login_data + f"{self.BASE_URL}/login", data=login_data ) if login_response.status_code != 200: - raise Exception(f"Login failed with status {login_response.status_code}") + raise Exception( + f"Login failed with status {login_response.status_code}" + ) logger.info("Successfully logged in to car image API") return True @@ -2472,39 +2516,38 @@ class CarImageAPIClient: try: headers = { - 'X-CSRFToken': self.csrf_token, - 'Referer': self.BASE_URL, + "X-CSRFToken": self.csrf_token, + "Referer": self.BASE_URL, } print(payload) generate_data = { - "year": payload['year'], - "make": payload['make'], - "model": payload['model'], - "exterior_color": payload['color'], + "year": payload["year"], + "make": payload["make"], + "model": payload["model"], + "exterior_color": payload["color"], "angle": "3/4 rear", - "reference_image": "" + "reference_image": "", } response = self.session.post( f"{self.BASE_URL}/generate", json=generate_data, headers=headers, - timeout=160 + timeout=160, ) response.raise_for_status() # Parse response result = response.json() - image_url = result.get('url') + image_url = result.get("url") if not image_url: raise Exception("No image URL in response") # Download the actual image image_response = self.session.get( - f"{self.BASE_URL}{image_url}", - timeout=160 + f"{self.BASE_URL}{image_url}", timeout=160 ) image_response.raise_for_status() @@ -2520,9 +2563,11 @@ class CarImageAPIClient: logger.error(error_msg) return None, error_msg + # Global client instance api_client = CarImageAPIClient() + def resize_image(image_data, max_size=(800, 600)): """ Resize image to make it smaller while maintaining aspect ratio @@ -2539,29 +2584,31 @@ def resize_image(image_data, max_size=(800, 600)): # Save back to bytes in original format output_buffer = BytesIO() - if original_format and original_format.upper() in ['JPEG', 'JPG']: - img.save(output_buffer, format='JPEG', quality=95, optimize=True) - elif original_format and original_format.upper() == 'PNG': + if original_format and original_format.upper() in ["JPEG", "JPG"]: + img.save(output_buffer, format="JPEG", quality=95, optimize=True) + elif original_format and original_format.upper() == "PNG": # Preserve transparency for PNG - if original_mode == 'RGBA': - img.save(output_buffer, format='PNG', optimize=True) + if original_mode == "RGBA": + img.save(output_buffer, format="PNG", optimize=True) else: - img.save(output_buffer, format='PNG', optimize=True) + img.save(output_buffer, format="PNG", optimize=True) else: # Default to JPEG for other formats - if img.mode in ('RGBA', 'LA', 'P'): + if img.mode in ("RGBA", "LA", "P"): # Convert to RGB if image has transparency - background = Image.new('RGB', img.size, (255, 255, 255)) - if img.mode == 'RGBA': + background = Image.new("RGB", img.size, (255, 255, 255)) + if img.mode == "RGBA": background.paste(img, mask=img.split()[3]) else: background.paste(img, (0, 0)) img = background - img.save(output_buffer, format='JPEG', quality=95, optimize=True) + img.save(output_buffer, format="JPEG", quality=95, optimize=True) resized_data = output_buffer.getvalue() - logger.info(f"Resized image from {len(image_data)} to {len(resized_data)} bytes") + logger.info( + f"Resized image from {len(image_data)} to {len(resized_data)} bytes" + ) return resized_data, None except Exception as e: @@ -2569,6 +2616,7 @@ def resize_image(image_data, max_size=(800, 600)): logger.error(error_msg) return None, error_msg + def generate_car_image_simple(car_image): """ Simple function to generate car image with authentication and resizing @@ -2577,10 +2625,10 @@ def generate_car_image_simple(car_image): # Prepare payload payload = { - 'make': car.id_car_make.name if car.id_car_make else '', - 'model': car.id_car_model.name if car.id_car_model else '', - 'year': car.year, - 'color': car.colors.exterior.name + "make": car.id_car_make.name if car.id_car_make else "", + "model": car.id_car_model.name if car.id_car_model else "", + "year": car.year, + "color": car.colors.exterior.name, } logger.info(f"Generating image for car {car.vin}") @@ -2589,10 +2637,10 @@ def generate_car_image_simple(car_image): image_data, error = api_client.generate_image(payload) if error: - return {'success': False, 'error': error} + return {"success": False, "error": error} if not image_data: - return {'success': False, 'error': 'No image data received'} + return {"success": False, "error": "No image data received"} try: # Resize the image to make it smaller @@ -2606,21 +2654,21 @@ def generate_car_image_simple(car_image): # Determine file extension based on content try: img = Image.open(BytesIO(resized_data)) - file_extension = img.format.lower() if img.format else 'jpg' + file_extension = img.format.lower() if img.format else "jpg" except: - file_extension = 'jpg' + file_extension = "jpg" # Save the resized image car_image.image.save( f"{car_image.image_hash}.{file_extension}", ContentFile(resized_data), - save=False + save=False, ) logger.info(f"Successfully generated and resized image for car {car.vin}") - return {'success': True} + return {"success": True} except Exception as e: error_msg = f"Image processing failed: {e}" logger.error(error_msg) - return {'success': False, 'error': error_msg} \ No newline at end of file + return {"success": False, "error": error_msg} diff --git a/inventory/validators.py b/inventory/validators.py index a3a9c8aa..f5fc43c7 100644 --- a/inventory/validators.py +++ b/inventory/validators.py @@ -2,13 +2,15 @@ from django.core.validators import RegexValidator from django.utils.translation import gettext_lazy as _ import re + class SaudiPhoneNumberValidator(RegexValidator): def __init__(self, *args, **kwargs): super().__init__( regex=r"^(\+9665|05)[0-9]{8}$", message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"), ) + def __call__(self, value): # Remove any whitespace, dashes, or other separators - cleaned_value = re.sub(r'[\s\-\(\)\.]', '', str(value)) - super().__call__(cleaned_value) \ No newline at end of file + cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value)) + super().__call__(cleaned_value) diff --git a/inventory/views.py b/inventory/views.py index 294fda00..cb935c33 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -17,7 +17,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 @@ -35,6 +35,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 @@ -107,7 +108,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, @@ -173,7 +174,7 @@ from django_ledger.models import ( BillModel, LedgerModel, PurchaseOrderModel, - ChartOfAccountModel + ChartOfAccountModel, ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -288,8 +289,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 @@ -362,6 +365,7 @@ def dealer_signup(request): "account/signup-wizard.html", ) + class HomeView(LoginRequiredMixin, TemplateView): """ HomeView class responsible for rendering the home page. @@ -397,25 +401,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 @@ -423,104 +428,149 @@ 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 - expenses = models.ItemModel.objects.filter(entity__admin__dealer=dealer, item_role='expense') - total_expenses = expenses.aggregate(total=Sum('default_amount'))['total'] or 0 + expenses = models.ItemModel.objects.filter( + entity__admin__dealer=dealer, item_role="expense" + ) + total_expenses = expenses.aggregate(total=Sum("default_amount"))["total"] or 0 gross_profit = net_profit_from_cars - 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) @@ -529,222 +579,215 @@ 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 ) - # ---------------------------------------------------- # 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, + "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, } - return render(request, 'dashboards/sales_dashboard.html', context) - - + return render(request, "dashboards/sales_dashboard.html", context) def aging_inventory_list_view(request, dealer_slug): @@ -756,49 +799,78 @@ 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') - total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total'] + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days), + ).exclude(status="sold") + total_aging_inventory_value = aging_cars_queryset.aggregate( + total=Sum("cost_price") + )["total"] # 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) - # 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. @@ -808,21 +880,21 @@ 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 - + "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, } - 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") @@ -847,7 +919,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. @@ -869,7 +943,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) @@ -1267,7 +1341,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. @@ -1291,7 +1367,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"]) @@ -1724,7 +1800,7 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): permission_required = ["inventory.view_car"] -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) @@ -1738,7 +1814,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 @@ -1769,7 +1850,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) @@ -2367,7 +2451,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. @@ -2389,7 +2474,6 @@ class StaffDetailView(LoginRequiredMixin, DetailView): context_object_name = "staff" - def dealer_vat_rate_update(request, slug): dealer = get_object_or_404(models.Dealer, slug=slug) models.VatRate.objects.filter(dealer=dealer).update(rate=request.POST.get("rate")) @@ -2487,6 +2571,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 @@ -2516,13 +2601,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() @@ -2637,8 +2720,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( @@ -2817,16 +2899,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, + }, ) @@ -2863,7 +2954,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, @@ -3651,7 +3744,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() @@ -3661,7 +3754,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]) @@ -3845,8 +3940,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( @@ -4354,6 +4448,7 @@ class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_user_type(self.request) accounts = dealer.entity.get_all_accounts() return apply_search_filters(accounts, query) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs @@ -4423,17 +4518,27 @@ class AccountCreateView( 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: - kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + kwargs["coa_model"] = ( + ChartOfAccountModel.objects.get(pk=coa_pk) + or self.request.entity.get_default_coa() + ) except Exception: kwargs["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 @@ -4535,21 +4640,30 @@ class AccountUpdateView( 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: - kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + kwargs["coa_model"] = ( + ChartOfAccountModel.objects.get(pk=coa_pk) + or self.request.entity.get_default_coa() + ) except Exception: kwargs["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 @@ -4594,17 +4708,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(order_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() paginator = Paginator(qs, 30) @@ -4692,11 +4808,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( @@ -4706,11 +4824,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 @@ -4723,12 +4841,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 @@ -4765,7 +4883,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", []) @@ -4856,7 +4976,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 } ) @@ -4979,7 +5101,7 @@ def create_estimate(request, dealer_slug, slug=None): .annotate(hash_count=Count("hash")) .distinct() ) - + context = { "form": form, "items": [ @@ -5035,7 +5157,7 @@ class EstimateDetailView(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) invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["data"] = finance_data @@ -5045,7 +5167,9 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView car = estimate.get_itemtxs_data()[0].first().item_model.car selected_items = car.additional_services.filter(dealer=dealer) 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 except Exception as e: @@ -5059,8 +5183,8 @@ 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" + template_name = "sales/estimates/estimate_preview.html" @login_required @@ -5114,8 +5238,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") @@ -5136,7 +5262,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", @@ -5157,14 +5283,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)}) @@ -5181,9 +5314,7 @@ 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"] - ) + car.additional_services.set(form.cleaned_data["additional_finances"]) car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5208,7 +5339,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) @@ -5306,10 +5437,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) @@ -5448,9 +5578,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) @@ -5491,7 +5623,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 @@ -5587,7 +5719,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, + }, ) @@ -5635,7 +5771,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): @@ -5643,7 +5783,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() @@ -5675,12 +5820,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 @@ -5720,7 +5875,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: { @@ -5744,7 +5899,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( @@ -5796,7 +5956,7 @@ 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 return super().get_context_data(**kwargs) @@ -5804,8 +5964,9 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView # payments + class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): - template_name = 'sales/invoices/invoice_update.html' + template_name = "sales/invoices/invoice_update.html" permission_required = ["django_ledger.change_invoicemodel"] @@ -5831,6 +5992,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): @@ -5877,7 +6039,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(): @@ -6054,7 +6221,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 @@ -6259,8 +6431,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( @@ -6279,8 +6450,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( @@ -6301,7 +6471,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}" @@ -6352,8 +6524,8 @@ def lead_create(request, dealer_slug): form.fields["id_car_make"].queryset = qs form.fields["id_car_make"].choices = [ (obj.id_car_make, obj.get_local_name()) for obj in qs - ] - + ] + if first_make := qs.first(): form.fields["id_car_model"].queryset = first_make.carmodel_set.all() @@ -6374,7 +6546,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") @@ -6387,7 +6559,7 @@ def lead_tracking(request, dealer_slug): "won": won, "lose": lose, "negotiation": negotiation, - "leads":leads + "leads": leads, } return render(request, "crm/leads/lead_tracking.html", context) @@ -6418,10 +6590,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 @@ -6429,7 +6598,6 @@ def update_lead_actions(request, dealer_slug): # Get the lead - # Update lead fields lead.status = current_action @@ -6458,10 +6626,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 @@ -6473,10 +6638,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"} @@ -6488,10 +6650,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: @@ -6501,10 +6660,7 @@ 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) @@ -6798,7 +6954,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 @@ -6846,7 +7002,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 @@ -6854,15 +7016,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 @@ -6902,7 +7066,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 @@ -6988,7 +7152,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") @@ -7043,7 +7207,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}, @@ -7142,9 +7306,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 @@ -7192,13 +7354,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 @@ -7214,6 +7375,7 @@ class OpportunityUpdateView( }, ) + class OpportunityStageUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): @@ -7244,7 +7406,6 @@ class OpportunityStageUpdateView( success_message = _("Opportunity Stage updated successfully.") permission_required = ["inventory.change_opportunity"] - def get_success_url(self): return reverse_lazy( "opportunity_detail", @@ -7367,9 +7528,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") @@ -7386,7 +7547,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( @@ -7641,16 +7802,17 @@ 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 ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class ItemExpenseCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ Represents a view for creating item expense entries. @@ -7741,9 +7903,6 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV ) - - - class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -7810,8 +7969,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): @@ -7820,7 +7981,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" @@ -9158,7 +9321,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. @@ -9286,7 +9448,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. @@ -9350,16 +9512,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. @@ -9484,7 +9647,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 @@ -9505,10 +9668,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", @@ -9697,15 +9860,16 @@ 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) if not dealer.active_plan: - plan_list = PlanPricing.objects.all() - form = forms.PaymentPlanForm() - return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) + plan_list = PlanPricing.objects.all() + 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 @@ -9745,41 +9909,47 @@ def payment_callback(request, dealer_slug): payment_id = request.GET.get("id") history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() payment_status = request.GET.get("status") - logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") + logger.info( + f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}" + ) order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW print(order) if payment_status == "paid": - logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.") + logger.info( + f"Payment successful for transaction ID {payment_id}. Processing order completion." + ) 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 " ", - } + "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}.") - if not hasattr(order.user, '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) + 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}." ) - 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}.") try: - # if order.user.userplan: # user = order.user # pricing = order.get_plan_pricing().pricing @@ -9794,28 +9964,36 @@ def payment_callback(request, dealer_slug): order.complete_order() history.status = "paid" history.save() - logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.") + logger.info( + f"Order {order.id} for user {order.user} completed successfully. Payment history updated." + ) invoice = order.get_invoices().first() 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} for user {order.user}: {e}") + logger.exception( + f"Error completing order {order.id} for user {order.user}: {e}" + ) logger.error(f"Plan activation failed: {str(e)}") history.status = "failed" history.save() - return render(request, "payment_failed.html", {"message": "Plan activation error"}) + return render( + request, "payment_failed.html", {"message": "Plan activation error"} + ) elif payment_status == "failed": - logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") + logger.warning( + f"Payment failed for transaction ID {payment_id}. Message: {message}" + ) history.status = "failed" history.save() return render(request, "payment_failed.html", {"message": message}) return render(request, "payment_failed.html", {"message": "Unknown payment status"}) + + # def payment_callback(request, dealer_slug): # message = request.GET.get("message") # dealer = get_object_or_404(models.Dealer, slug=dealer_slug) @@ -10036,7 +10214,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): @@ -10150,10 +10327,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) @@ -10593,7 +10772,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( @@ -10621,14 +10806,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 @@ -10767,7 +10956,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, @@ -10897,12 +11088,12 @@ def upload_cars(request, dealer_slug, pk=None): cost_price=po_item.item.unit_cost, ) # if po_item: #TODO:update - # models.CarFinance.objects.create( - # car=car, - # cost_price=po_item.item.unit_cost, - # marked_price=0, - # selling_price=0, - # ) + # models.CarFinance.objects.create( + # car=car, + # cost_price=po_item.item.unit_cost, + # marked_price=0, + # selling_price=0, + # ) car.add_colors(exterior=exterior, interior=interior) cars_created += 1 logger.debug( @@ -10987,64 +11178,72 @@ class InventoryListView(InventoryListViewBase): template_name = "inventory/list.html" permission_required = ["django_ledger.view_purchaseordermodel"] + @login_required -def purchase_report_view(request,dealer_slug): +def purchase_report_view(request, dealer_slug): pos = request.entity.get_purchase_orders() data = [] - total_po_amount=0 - total_po_cars=0 + total_po_amount = 0 + 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=0 - po_quantity=0 + po_amount = 0 + po_quantity = 0 for item in items: - po_amount+=item["total"] - po_quantity+=item["q"] + po_amount += item["total"] + po_quantity += item["q"] - total_po_amount+=po_amount - total_po_cars+=po_quantity - bills=po.get_po_bill_queryset() - vendors=set([bill.vendor.vendor_name for bill in bills]) + total_po_amount += po_amount + total_po_cars += po_quantity + bills = po.get_po_bill_queryset() + 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={ - "dealer":request.entity.name, - "time":current_time, - "data":data, - "total_po_amount":total_po_amount, - "total_po_cars":total_po_cars, - "current_time":current_time - + context = { + "dealer": request.entity.name, + "time": current_time, + "data": data, + "total_po_amount": total_po_amount, + "total_po_cars": total_po_cars, + "current_time": current_time, } + 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') - +def purchase_report_csv_export(request, dealer_slug): + 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() @@ -11052,45 +11251,47 @@ 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"] po_quantity += item["q"] bills = po.get_po_bill_queryset() - vendors = set([bill.vendor.vendor_name for bill in bills ]) + 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 - @login_required def car_sale_report_view(request, dealer_slug): 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 - - cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() + VAT_RATE = vat.rate + 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') + 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") # Apply filters to the queryset if selected_make: @@ -11104,89 +11305,124 @@ def car_sale_report_view(request, dealer_slug): if selected_stock_type: cars_sold = cars_sold.filter(stock_type=selected_stock_type) - # # 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_cars_sold = cars_sold.count() + 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_vat_collected = total_vat_on_cars+total_vat_from_additonals - total_revenue_collected=total_revenue_from_cars+total_revenue_from_additonals + 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 = sum([car.discount for car in cars_sold]) current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") # Get distinct values for filter dropdowns - 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() - models_qs =base_sold_cars_queryset.values_list('id_car_model__name', flat=True).distinct() + 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() + models_qs = base_sold_cars_queryset.values_list( + "id_car_model__name", flat=True + ).distinct() - series =base_sold_cars_queryset.values_list('id_car_serie__name', flat=True).distinct() - stock_types=base_sold_cars_queryset.values_list('stock_type', flat=True).distinct() - years = base_sold_cars_queryset.values_list('year', flat=True).distinct().order_by('-year') + series = base_sold_cars_queryset.values_list( + "id_car_serie__name", flat=True + ).distinct() + stock_types = base_sold_cars_queryset.values_list( + "stock_type", flat=True + ).distinct() + years = ( + base_sold_cars_queryset.values_list("year", flat=True) + .distinct() + .order_by("-year") + ) 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, - 'models': models_qs, - 'series': series, - 'years': years, - 'stock_types':stock_types, - 'selected_make': selected_make, - 'selected_model': selected_model, - 'selected_serie': selected_serie, - 'selected_year': selected_year, - 'selected_stock_type':selected_stock_type, + "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, + "models": models_qs, + "series": series, + "years": years, + "stock_types": stock_types, + "selected_make": selected_make, + "selected_model": selected_model, + "selected_serie": selected_serie, + "selected_year": selected_year, + "selected_stock_type": selected_stock_type, } - return render(request, 'ledger/reports/car_sale_report.html', context) + return render(request, "ledger/reports/car_sale_report.html", context) @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') + 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") if selected_make: cars_sold = cars_sold.filter(id_car_make__name=selected_make) @@ -11203,8 +11439,8 @@ def car_sale_report_csv_export(request, dealer_slug): for car in cars_sold: # Fetching data for the additional services 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"] # Checking for the invoice number to avoid errors on cars without one invoice_number = None @@ -11213,27 +11449,29 @@ 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, # Ensure this property returns a number - car.final_price, # Selling Price without VAT - car.vat_amount, # VAT on the car - services_total_price, # Total services without VAT - services_vat_amount, # VAT on services - 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, # Ensure this property returns a number + car.final_price, # Selling Price without VAT + car.vat_amount, # VAT on the car + services_total_price, # Total services without VAT + services_vat_amount, # VAT on services + car.final_price_plus_services_plus_vat, + invoice_number, + ] + ) return response @@ -11244,94 +11482,109 @@ 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, _('Invalid password. Please try again.')) + messages.error(request, _("Invalid password. Please try again.")) 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 @@ -11367,137 +11620,156 @@ 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' + 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' + 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' + 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' + template_name = "chart_of_accounts/coa_update.html" + permission_required = "django_ledger.change_chartofaccountmodel" + + class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): - permission_required = 'django_ledger.change_chartofaccountmodel' + permission_required = "django_ledger.change_chartofaccountmodel" diff --git a/load_json_data.py b/load_json_data.py index a112824d..8435831e 100644 --- a/load_json_data.py +++ b/load_json_data.py @@ -46,7 +46,7 @@ def run(): # arabic_name=item.get("arabic_name", ""), # logo=item.get("Logo", ""), # is_sa_import=item.get("is_sa_import", False), - slug=unique_slug + slug=unique_slug, ) # Step 2: Insert CarModel @@ -60,7 +60,7 @@ def run(): id_car_make_id=item["id_car_make"], name=item["name"], # arabic_name=item.get("arabic_name", ""), - slug=unique_slug + slug=unique_slug, ) # Step 3: Insert CarSerie @@ -77,7 +77,7 @@ def run(): year_begin=item.get("year_begin"), year_end=item.get("year_end"), generation_name=item.get("generation_name", ""), - slug=unique_slug + slug=unique_slug, ) # Step 4: Insert CarTrim @@ -98,9 +98,10 @@ def run(): # Step 5: Insert CarEquipment - for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"): - if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists(): + if not CarEquipment.objects.filter( + id_car_equipment=item["id_car_equipment"] + ).exists(): if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists(): unique_slug = generate_unique_slug(CarEquipment, item["name"]) CarEquipment.objects.create( @@ -108,7 +109,7 @@ def run(): id_car_trim_id=item["id_car_trim"], name=item["name"], year_begin=item.get("year"), - slug=unique_slug + slug=unique_slug, ) # Step 6: Insert CarSpecification (Parent specifications first) diff --git a/scripts/set_plans.py b/scripts/set_plans.py index 0fb47a05..374d6c4c 100644 --- a/scripts/set_plans.py +++ b/scripts/set_plans.py @@ -34,7 +34,7 @@ def run(): is_boolean=True, url="pricing", ) - # Create the plans + # Create the plans free_plan = Plan.objects.create( name="Free", description="Free plan with limited features", @@ -46,7 +46,7 @@ def run(): order=1, ) free_plan.quotas.add(free_quota) - + # Create the plans basic_plan = Plan.objects.create( name="Basic", @@ -58,7 +58,7 @@ def run(): visible=True, order=1, ) - basic_plan.quotas.add(basic_quota,free_quota) + basic_plan.quotas.add(basic_quota, free_quota) pro_plan = Plan.objects.create( name="Professional", @@ -69,7 +69,7 @@ def run(): visible=True, # order=2 ) - pro_plan.quotas.add(free_quota,basic_quota, pro_quota) + pro_plan.quotas.add(free_quota, basic_quota, pro_quota) premium_plan = Plan.objects.create( name="Premium", @@ -80,4 +80,4 @@ def run(): visible=True, order=3, ) - premium_plan.quotas.add(free_quota,basic_quota, pro_quota, premium_quota) + premium_plan.quotas.add(free_quota, basic_quota, pro_quota, premium_quota) diff --git a/templates/403.html b/templates/403.html index 0f572060..8eb9a6c3 100644 --- a/templates/403.html +++ b/templates/403.html @@ -1,177 +1,175 @@ {% load i18n %} - - - - 403 - Access Forbidden - - - - - -
- -
-

403

-

{% trans "Access Forbidden" %}

-

{% trans "You do not have permission to view this page."%}

-

{% trans "Powered By Tenhal, Riyadh Saudi Arabia"%}

- {% trans "Go Home" %} -
- - - - + + - - \ No newline at end of file + }, + "interactivity": { + "detect_on": "canvas", + "events": { + "onhover": { + "enable": true, + "mode": "grab" + }, + "onclick": { + "enable": true, + "mode": "push" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 140, + "line_linked": { + "opacity": 1 + } + }, + "push": { + "particles_nb": 4 + } + } + }, + "retina_detect": true + }); + + + diff --git a/templates/404.html b/templates/404.html index 18b5f806..3ff33b8c 100644 --- a/templates/404.html +++ b/templates/404.html @@ -55,27 +55,27 @@ rel="stylesheet" id="user-style-default" /> diff --git a/templates/500.html b/templates/500.html index 1b1d2d9f..d2857507 100644 --- a/templates/500.html +++ b/templates/500.html @@ -55,27 +55,27 @@ rel="stylesheet" id="user-style-default" /> diff --git a/templates/account/lock-screen.html b/templates/account/lock-screen.html index 2563a4bd..5447cb7a 100644 --- a/templates/account/lock-screen.html +++ b/templates/account/lock-screen.html @@ -61,19 +61,19 @@ rel="stylesheet" id="user-style-default"> @@ -109,17 +109,17 @@
diff --git a/templates/account/login.html b/templates/account/login.html index 0c82f862..0a720c0b 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -6,7 +6,6 @@ {% trans "Sign In" %} {% endblock head_title %} {% block content %} -
@@ -78,9 +77,7 @@
- - {% include 'footer.html' %} - + {% include 'footer.html' %} {% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
{% element button_group vertical=True %} diff --git a/templates/account/signup-wizard.html b/templates/account/signup-wizard.html index e96b99c5..95c43bcc 100644 --- a/templates/account/signup-wizard.html +++ b/templates/account/signup-wizard.html @@ -282,9 +282,7 @@
- - {% include 'footer.html' %} - + {% include 'footer.html' %} {% endblock content %} {% block customJS %} @@ -293,97 +291,97 @@ {% endblock customJS %} diff --git a/templates/account/user_settings.html b/templates/account/user_settings.html index 0700d665..9a3243da 100644 --- a/templates/account/user_settings.html +++ b/templates/account/user_settings.html @@ -6,42 +6,42 @@ {% trans 'Dealer Settings' %} {% endblock %} {% block content %} -
-
-
-
-

- {% trans "Dealer Settings" %} - -

-
-
-
- {% csrf_token %} -
-
-

{% trans 'Default Invoice Accounts' %}

- {{ form.invoice_cash_account|as_crispy_field }} - {{ form.invoice_prepaid_account|as_crispy_field }} - {{ form.invoice_unearned_account|as_crispy_field }} +
+
+
+
+

+ {% trans "Dealer Settings" %} + +

+
+
+ + {% csrf_token %} +
+
+

{% trans 'Default Invoice Accounts' %}

+ {{ form.invoice_cash_account|as_crispy_field }} + {{ form.invoice_prepaid_account|as_crispy_field }} + {{ form.invoice_unearned_account|as_crispy_field }} +
+
+

{% trans 'Default Bill Accounts' %}

+ {{ form.bill_cash_account|as_crispy_field }} + {{ form.bill_prepaid_account|as_crispy_field }} + {{ form.bill_unearned_account|as_crispy_field }} +
-
-

{% trans 'Default Bill Accounts' %}

- {{ form.bill_cash_account|as_crispy_field }} - {{ form.bill_prepaid_account|as_crispy_field }} - {{ form.bill_unearned_account|as_crispy_field }} +
+
+
-
-
-
- -
- + +
-
-
-{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/templates/admin_management/confirm_activate_account.html b/templates/admin_management/confirm_activate_account.html index dae88d78..7f5c63ba 100644 --- a/templates/admin_management/confirm_activate_account.html +++ b/templates/admin_management/confirm_activate_account.html @@ -23,7 +23,7 @@

{% trans 'Activate Account' %}

-

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

+

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

{% csrf_token %}
diff --git a/templates/admin_management/management.html b/templates/admin_management/management.html index efd75f84..0733b645 100644 --- a/templates/admin_management/management.html +++ b/templates/admin_management/management.html @@ -1,29 +1,33 @@ {% extends "base.html" %} {% load i18n %} {% block title %} - {% trans 'Admin Management' %} {% endblock %} - {% block content %} -

{% trans "Admin Management" %}
  • -
    -
    - - +{% endblock content %} diff --git a/templates/administration/email_change_verification_code.html b/templates/administration/email_change_verification_code.html index 14d4993b..16cdcf59 100644 --- a/templates/administration/email_change_verification_code.html +++ b/templates/administration/email_change_verification_code.html @@ -32,15 +32,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/administration/manage_day_off.html b/templates/administration/manage_day_off.html index 759e655a..f6c31405 100644 --- a/templates/administration/manage_day_off.html +++ b/templates/administration/manage_day_off.html @@ -78,15 +78,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/administration/manage_staff_member.html b/templates/administration/manage_staff_member.html index 30ed1254..0f3ec181 100644 --- a/templates/administration/manage_staff_member.html +++ b/templates/administration/manage_staff_member.html @@ -65,15 +65,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/administration/manage_staff_personal_info.html b/templates/administration/manage_staff_personal_info.html index 1ab7d0af..579362f3 100644 --- a/templates/administration/manage_staff_personal_info.html +++ b/templates/administration/manage_staff_personal_info.html @@ -25,15 +25,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/administration/manage_working_hours.html b/templates/administration/manage_working_hours.html index bced3149..2dcaeda1 100644 --- a/templates/administration/manage_working_hours.html +++ b/templates/administration/manage_working_hours.html @@ -11,22 +11,9 @@ method="post" action="" id="workingHoursForm" - data-action="{% if working_hours_instance %} - update - {% else %} - create - {% endif %}" - data-working-hours-id=" - {% if working_hours_instance %} - {{ working_hours_instance.id }} - {% else %} - 0 - {% endif %}" - data-staff-user-id="{% if staff_user_id %} - {{ staff_user_id }} - {% else %} - 0 - {% endif %}"> + data-action="{% if working_hours_instance %} update {% else %} create {% endif %}" + data-working-hours-id=" {% if working_hours_instance %} {{ working_hours_instance.id }} {% else %} 0 {% endif %}" + data-staff-user-id="{% if staff_user_id %} {{ staff_user_id }} {% else %} 0 {% endif %}"> {% csrf_token %} {% if working_hours_form.staff_member %}
    @@ -94,15 +81,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/administration/staff_index.html b/templates/administration/staff_index.html index 5a123616..bbc8a578 100644 --- a/templates/administration/staff_index.html +++ b/templates/administration/staff_index.html @@ -23,15 +23,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/appointment/appointments.html b/templates/appointment/appointments.html index 51911bd4..8643336e 100644 --- a/templates/appointment/appointments.html +++ b/templates/appointment/appointments.html @@ -68,7 +68,9 @@ {% endif %} {% for sf in all_staff_members %} + {% if staff_member and staff_member.id == sf.id %}selected{% endif %}> + {{ sf.get_staff_member_name }} + {% endfor %}
    @@ -88,15 +90,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/appointment/default_thank_you.html b/templates/appointment/default_thank_you.html index 1f927faa..3aef64d8 100644 --- a/templates/appointment/default_thank_you.html +++ b/templates/appointment/default_thank_you.html @@ -24,15 +24,7 @@ {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/appointment/enter_verification_code.html b/templates/appointment/enter_verification_code.html index 9eb7d8a3..c81a9673 100644 --- a/templates/appointment/enter_verification_code.html +++ b/templates/appointment/enter_verification_code.html @@ -29,8 +29,7 @@ {% if messages %} {% for message in messages %} -
    {{ message }}
    +
    {{ message }}
    {% endfor %} {% endif %}
    diff --git a/templates/appointment/rescheduling_thank_you.html b/templates/appointment/rescheduling_thank_you.html index 4b90ef66..fc2e1874 100644 --- a/templates/appointment/rescheduling_thank_you.html +++ b/templates/appointment/rescheduling_thank_you.html @@ -69,15 +69,7 @@
    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/appointment/thank_you.html b/templates/appointment/thank_you.html index 314343b3..2fe13452 100644 --- a/templates/appointment/thank_you.html +++ b/templates/appointment/thank_you.html @@ -40,15 +40,7 @@

    {{ page_message }}

    {% if messages %} {% for message in messages %} - {% endfor %} {% endif %} diff --git a/templates/auth_base.html b/templates/auth_base.html index 15f6c912..47b133a7 100644 --- a/templates/auth_base.html +++ b/templates/auth_base.html @@ -3,11 +3,7 @@ {% get_current_language as LANGUAGE_CODE %} diff --git a/templates/base.html b/templates/base.html index b875891d..7c8b42c8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,11 +2,7 @@ {% get_current_language as LANGUAGE_CODE %} @@ -54,8 +50,14 @@ {% comment %} {% endcomment %} {% if LANGUAGE_CODE == 'ar' %} - - + + {% else %} {% endif %} - - - {% comment %} {% endcomment %} {% comment %} {% block customCSS %}{% endblock %} {% endcomment %} @@ -85,19 +84,25 @@ {% block period_navigation %} {% endblock period_navigation %}
    - -
    -
    + +
    +
    {% block customCSS %}{% endblock %} - {% block content %}{% endblock content %} + {% block content %} + {% endblock content %} {% block customJS %}{% endblock %} - {% comment %} - - + + {% endcomment %} -
    {% block body %} {% endblock body %} @@ -126,7 +131,7 @@ {% comment %} {% endcomment %} {% comment %} {% endcomment %} {% comment %} - + {% endcomment %} {% comment %} {% endcomment %} {% comment %} {% endcomment %} @@ -156,57 +161,57 @@ document.getElementById('global-indicator') ]; });*/ - let Toast = Swal.mixin({ - toast: true, - position: "top-end", - showConfirmButton: false, - timer: 3000, - timerProgressBar: true, - didOpen: (toast) => { - toast.onmouseenter = Swal.stopTimer; - toast.onmouseleave = Swal.resumeTimer; - } - }); - function notify(tag, msg) { - Toast.fire({ - icon: tag, - titleText: msg - }); - } -document.addEventListener('htmx:afterRequest', function(evt) { - if(evt.detail.xhr.status == 403){ - /* Notify the user of a 404 Not Found response */ - notify("error", "You do not have permission to view this page"); - } - if(evt.detail.xhr.status == 404){ - /* Notify the user of a 404 Not Found response */ - return alert("Error: Could Not Find Resource"); - } - if (evt.detail.successful != true) { - console.log(evt.detail.xhr.statusText) - /* Notify of an unexpected error, & print error to console */ - notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`); - } -}); -document.body.addEventListener('htmx:beforeSwap', function(evt) { - if (evt.detail.target.id === 'main_content') { - var backdrops = document.querySelectorAll('.modal-backdrop'); - backdrops.forEach(function(backdrop) { - backdrop.remove(); - }); - } - }); - // Close modal after successful form submission - document.body.addEventListener('htmx:afterSwap', function(evt) { - if (evt.detail.target.id === 'main_content') { - document.querySelectorAll('.modal').forEach(function(m) { - var modal = bootstrap.Modal.getInstance(m); - if (modal) { - modal.hide(); + let Toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + didOpen: (toast) => { + toast.onmouseenter = Swal.stopTimer; + toast.onmouseleave = Swal.resumeTimer; + } + }); + function notify(tag, msg) { + Toast.fire({ + icon: tag, + titleText: msg + }); } - }); - } - }); + document.addEventListener('htmx:afterRequest', function(evt) { + if(evt.detail.xhr.status == 403){ + /* Notify the user of a 404 Not Found response */ + notify("error", "You do not have permission to view this page"); + } + if(evt.detail.xhr.status == 404){ + /* Notify the user of a 404 Not Found response */ + return alert("Error: Could Not Find Resource"); + } + if (evt.detail.successful != true) { + console.log(evt.detail.xhr.statusText) + /* Notify of an unexpected error, & print error to console */ + notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`); + } + }); + document.body.addEventListener('htmx:beforeSwap', function(evt) { + if (evt.detail.target.id === 'main_content') { + var backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(function(backdrop) { + backdrop.remove(); + }); + } + }); + // Close modal after successful form submission + document.body.addEventListener('htmx:afterSwap', function(evt) { + if (evt.detail.target.id === 'main_content') { + document.querySelectorAll('.modal').forEach(function(m) { + var modal = bootstrap.Modal.getInstance(m); + if (modal) { + modal.hide(); + } + }); + } + }); {% comment %} {% block customJS %}{% endblock %} {% endcomment %} diff --git a/templates/bill/bill_create.html b/templates/bill/bill_create.html index e86a5929..e5da5260 100644 --- a/templates/bill/bill_create.html +++ b/templates/bill/bill_create.html @@ -6,48 +6,48 @@ {% block title %} {{ _("Create Bill") |capfirst }} {% endblock title %} - {% block content %} -
    -
    -
    -
    -

    - {% trans 'Create Bill' %} - -

    -
    -
    -
    - {% csrf_token %} - {% if po_model %} -
    -

    {% trans 'Bill for' %} {{ po_model.po_number }}

    -

    {% trans 'Bill for' %} {{ po_model.po_title }}

    -
    - {% for itemtxs in po_itemtxs_qs %} - {{ itemtxs }} - {% endfor %} +
    +
    +
    +
    +

    + {% trans 'Create Bill' %} + +

    +
    +
    + + {% csrf_token %} + {% if po_model %} +
    +

    {% trans 'Bill for' %} {{ po_model.po_number }}

    +

    {% trans 'Bill for' %} {{ po_model.po_title }}

    +
    + {% for itemtxs in po_itemtxs_qs %}{{ itemtxs }}{% endfor %} +
    + {% endif %} +
    {{ form|crispy }}
    +
    +
    + + + + {% trans "Cancel" %} +
    - {% endif %} -
    - {{ form|crispy }} -
    -
    -
    - - - - {% trans "Cancel" %} - -
    - + +
    -
    -
    -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/templates/bill/bill_detail.html b/templates/bill/bill_detail.html index 786a4561..bdaa39ae 100644 --- a/templates/bill/bill_detail.html +++ b/templates/bill/bill_detail.html @@ -4,180 +4,160 @@ {% load django_ledger %} {% load custom_filters %} {% block title %}Bill Details{% endblock %} - -{% block content%} - +{% block content %}
    -
    -
    - {% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %} -
    - {% if bill.is_configured %} - -
    -
    - -
    - {% trans 'Cash Account' %}: - {% if bill.cash_account %} - {{ bill.cash_account.code }} - {% else %} - {{ bill.cash_account.code }} - {% endif %} -
    -

    - {% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }} -

    - -
    - {% if bill.accrue %} -
    - -
    - {% trans 'Prepaid Account' %}: - {% if bill.prepaid_account %} - - {{ bill.prepaid_account.code }} - - {% else %} - {{ bill.prepaid_account.code }} - {% endif %} -
    -

    - {% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }} -

    - -
    -
    - -
    - {% trans 'Accounts Payable' %}: - {% if bill.unearned_account %} - - {{ bill.unearned_account.code }} - - {% else %} - {{ bill.unearned_account.code }} - {% endif %} -
    -

    - {% currency_symbol %}{{ bill.get_amount_unearned | currency_format }} -

    - -
    -
    - -
    {% trans 'Accrued' %} {{ bill.get_progress | percentage }}
    -

    {% currency_symbol %}{{ bill.get_amount_earned | currency_format }}

    - -
    - {% else %} -
    - -
    {% trans 'You Still Owe' %}
    -

    - {% currency_symbol %}{{ bill.get_amount_open | currency_format }} -

    - -
    - {% endif %} -
    - - {% endif %} +
    +
    + {% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
    -
    - -
    -
    -
    -
    - -
    {% trans 'Bill Items' %}
    -
    + {% if bill.is_configured %} +
    +
    +
    + {% trans 'Cash Account' %}: + {% if bill.cash_account %} + {{ bill.cash_account.code }} + {% else %} + {{ bill.cash_account.code }} + {% endif %} +
    +

    + {% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }} +

    -
    -
    - - - - - - - - - - - - - {% for bill_item in itemtxs_qs %} - - - - - - - - - {% endfor %} - - - - - - - - - -
    {% trans 'Item' %}{% trans 'Entity Unit' %}{% trans 'Unit Cost' %}{% trans 'Quantity' %}{% trans 'Total' %}{% trans 'PO' %}
    -
    -
    -
    {{ bill_item.item_model }}
    -
    -
    -
    - - {% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %} - - - {{ bill_item.unit_cost | currency_format }} - - {{ bill_item.quantity }} - - {{ bill_item.total_amount | currency_format }} - - {% if bill_item.po_model_id %} - {% if perms.django_ledger.view_purchaseordermodel %} - - {% trans 'View PO' %} - - {% endif %} - {% endif %} -
    - {% trans 'Total' %} - - {% currency_symbol %}{{ total_amount__sum | currency_format }} -
    + {% if bill.accrue %} +
    +
    + {% trans 'Prepaid Account' %}: + {% if bill.prepaid_account %} + {{ bill.prepaid_account.code }} + {% else %} + {{ bill.prepaid_account.code }} + {% endif %} +
    +

    + {% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }} +

    -
    +
    +
    + {% trans 'Accounts Payable' %}: + {% if bill.unearned_account %} + {{ bill.unearned_account.code }} + {% else %} + {{ bill.unearned_account.code }} + {% endif %} +
    +

    + {% currency_symbol %}{{ bill.get_amount_unearned | currency_format }} +

    +
    +
    +
    {% trans 'Accrued' %} {{ bill.get_progress | percentage }}
    +

    {% currency_symbol %}{{ bill.get_amount_earned | currency_format }}

    +
    + {% else %} +
    +
    {% trans 'You Still Owe' %}
    +

    + {% currency_symbol %}{{ bill.get_amount_open | currency_format }} +

    +
    + {% endif %}
    -
    - -
    -
    -
    -
    - -
    {% trans 'Bill Transactions' %}
    -
    -
    -
    {% transactions_table bill %}
    + {% endif %}
    -
    - {% include "bill/includes/mark_as.html" %} -{% endblock %} +
    +
    +
    +
    + +
    {% trans 'Bill Items' %}
    +
    +
    +
    +
    + + + + + + + + + + + + + {% for bill_item in itemtxs_qs %} + + + + + + + + + {% endfor %} + + + + + + + + + +
    {% trans 'Item' %}{% trans 'Entity Unit' %}{% trans 'Unit Cost' %}{% trans 'Quantity' %}{% trans 'Total' %}{% trans 'PO' %}
    +
    +
    +
    {{ bill_item.item_model }}
    +
    +
    +
    + + {% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %} + + + {{ bill_item.unit_cost | currency_format }} + + {{ bill_item.quantity }} + + {{ bill_item.total_amount | currency_format }} + + {% if bill_item.po_model_id %} + {% if perms.django_ledger.view_purchaseordermodel %} + + {% trans 'View PO' %} + + {% endif %} + {% endif %} +
    + {% trans 'Total' %} + + {% currency_symbol %}{{ total_amount__sum | currency_format }} +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    {% trans 'Bill Transactions' %}
    +
    +
    +
    {% transactions_table bill %}
    +
    +
    +
    + {% include "bill/includes/mark_as.html" %} + {% endblock %} diff --git a/templates/bill/bill_update.html b/templates/bill/bill_update.html index dad74f72..a4f52273 100644 --- a/templates/bill/bill_update.html +++ b/templates/bill/bill_update.html @@ -14,7 +14,8 @@
    {% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %} -
    + {% csrf_token %}
    {{ form|crispy }}
    + {% endif %} {% if bill.can_cancel %} {% endif %} @@ -219,7 +220,6 @@ {% endif %} {% if bill.can_review %} - - {% else %} - {% if bill.can_approve and perms.django_ledger.can_approve_billmodel %} - - {% endif %} - {% endif %} - - {% if bill.can_pay %} + {% if bill.can_approve and request.is_accountant %} + + {% else %} + {% if bill.can_approve and perms.django_ledger.can_approve_billmodel %} {% endif %} - - {% if bill.can_void %} - - {% endif %} - - {% if bill.can_cancel %} - - {% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %} - {% endif %} - + {% endif %} + + {% if bill.can_pay %} + + {% endif %} + + {% if bill.can_void %} + + {% endif %} + + {% if bill.can_cancel %} + + {% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %} + {% endif %} {% endif %}
    diff --git a/templates/bill/tags/bill_item_formset.html b/templates/bill/tags/bill_item_formset.html index 792a6ae8..eb0d0c55 100644 --- a/templates/bill/tags/bill_item_formset.html +++ b/templates/bill/tags/bill_item_formset.html @@ -2,137 +2,142 @@ {% load static %} {% load django_ledger %} {% load widget_tweaks %} - {% if bill.get_itemtxs_data.1.total_amount__sum > 0 %} - -{% else %} - -{% endif %} -
    - -
    -
    -

    - - {% trans 'Bill Items' %} -

    -
    + + {% else %} + + {% endif %} +
    + +
    +
    +

    + + {% trans 'Bill Items' %} +

    +
    +
    -
    - -
    -
    - {% csrf_token %} - {{ item_formset.non_form_errors }} - {{ item_formset.management_form }} - -
    -
    - -
    - - - - - - - - - - - - - - - {% for f in item_formset %} - - - - - - - - - - - - - - - - - + +
    +
    + {% csrf_token %} + {{ item_formset.non_form_errors }} + {{ item_formset.management_form }} + +
    +
    + +
    +
    {% trans 'Item' %}{% trans 'PO Qty' %}{% trans 'PO Amount' %}{% trans 'Quantity' %}{% trans 'Unit Cost' %}{% trans 'Unit' %}{% trans 'Total' %}{% trans 'Delete' %}
    -
    - {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %} - {{ f.item_model|add_class:"form-control" }} - {% if f.errors %}{{ f.errors }}{% endif %} -
    -
    - - {% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %} - - - {% if f.instance.po_total_amount %} -
    - {% currency_symbol %}{{ f.instance.po_total_amount | currency_format }} - - {% trans 'View PO' %} - -
    - {% endif %} -
    -
    {{ f.quantity|add_class:"form-control" }}
    -
    -
    {{ f.unit_cost|add_class:"form-control" }}
    -
    {{ f.entity_unit|add_class:"form-control" }} - - {% currency_symbol %}{{ f.instance.total_amount | currency_format }} - - - {% if item_formset.can_delete %}
    {{ f.DELETE }}
    {% endif %} -
    + + + + + + + + + + - {% endfor %} - - - - - - - - - - -
    {% trans 'Item' %}{% trans 'PO Qty' %}{% trans 'PO Amount' %}{% trans 'Quantity' %}{% trans 'Unit Cost' %}{% trans 'Unit' %}{% trans 'Total' %}{% trans 'Delete' %}
    - {% trans 'Total' %} - - {% currency_symbol %}{{ total_amount__sum | currency_format }} -
    + + + {% for f in item_formset %} + + + +
    + {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %} + {{ f.item_model|add_class:"form-control" }} + {% if f.errors %}{{ f.errors }}{% endif %} +
    + + + + + {% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %} + + + + + {% if f.instance.po_total_amount %} +
    + {% currency_symbol %}{{ f.instance.po_total_amount | currency_format }} + + {% trans 'View PO' %} + +
    + {% endif %} + + + +
    {{ f.quantity|add_class:"form-control" }}
    + + + +
    {{ f.unit_cost|add_class:"form-control" }}
    + + + {{ f.entity_unit|add_class:"form-control" }} + + + + {% currency_symbol %}{{ f.instance.total_amount | currency_format }} + + + + + {% if item_formset.can_delete %}
    {{ f.DELETE }}
    {% endif %} + + + {% endfor %} + + + + + + + {% trans 'Total' %} + + + {% currency_symbol %}{{ total_amount__sum | currency_format }} + + + + + +
    -
    - -
    -
    -
    - {% if not item_formset.has_po %} - - - {% trans 'New Item' %} - - {% endif %} - + +
    +
    +
    + {% if not item_formset.has_po %} + + + {% trans 'New Item' %} + + {% endif %} + +
    -
    - + diff --git a/templates/chart_of_accounts/coa_create.html b/templates/chart_of_accounts/coa_create.html index 98dc4f2b..a241471c 100644 --- a/templates/chart_of_accounts/coa_create.html +++ b/templates/chart_of_accounts/coa_create.html @@ -2,58 +2,52 @@ {% load i18n static %} {% load django_ledger %} {% load widget_tweaks %} - {% block content %} -
    -
    -
    -
    -

    - {% trans 'Create Chart of Accounts' %} - -

    -
    -
    -
    - {% csrf_token %} - - {# Bootstrap form rendering #} -
    - {{ form.name.label_tag }} - {{ form.name|add_class:"form-control" }} - {% if form.name.help_text %} - {{ form.name.help_text }} - {% endif %} - {% for error in form.name.errors %} -
    {{ error }}
    - {% endfor %} -
    -
    - {{ form.description.label_tag }} - {{ form.description|add_class:"form-control" }} - {% if form.description.help_text %} - {{ form.description.help_text }} - {% endif %} - {% for error in form.description.errors %} -
    {{ error }}
    - {% endfor %} -
    - -
    -
    - - - - {% trans 'Cancel' %} - -
    -
    +
    +
    +
    +
    +

    + {% trans 'Create Chart of Accounts' %} + +

    +
    +
    +
    + {% csrf_token %} + {# Bootstrap form rendering #} +
    + {{ form.name.label_tag }} + {{ form.name|add_class:"form-control" }} + {% if form.name.help_text %}{{ form.name.help_text }}{% endif %} + {% for error in form.name.errors %}
    {{ error }}
    {% endfor %} +
    +
    + {{ form.description.label_tag }} + {{ form.description|add_class:"form-control" }} + {% if form.description.help_text %} + {{ form.description.help_text }} + {% endif %} + {% for error in form.description.errors %}
    {{ error }}
    {% endfor %} +
    +
    +
    + + + + {% trans 'Cancel' %} + +
    +
    +
    -
    -
    -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/templates/chart_of_accounts/coa_list.html b/templates/chart_of_accounts/coa_list.html index 09f2c205..2938bb2b 100644 --- a/templates/chart_of_accounts/coa_list.html +++ b/templates/chart_of_accounts/coa_list.html @@ -2,7 +2,6 @@ {% load i18n %} {% load static %} {% load icon from django_ledger %} - {% block content %}
    @@ -10,17 +9,19 @@

    {% trans "Chart of Accounts" %}

    - + {% trans "Add New" %}
    - {% if not inactive %} - + {% trans 'Show Inactive' %} {% else %} - + {% trans 'Show Active' %} {% endif %} @@ -28,11 +29,9 @@
    {% for coa_model in coa_list %} -
    - {% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %} -
    +
    {% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
    {% endfor %}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/chart_of_accounts/coa_update.html b/templates/chart_of_accounts/coa_update.html index 433fd0b4..687531ec 100644 --- a/templates/chart_of_accounts/coa_update.html +++ b/templates/chart_of_accounts/coa_update.html @@ -2,23 +2,20 @@ {% load i18n %} {% load static %} {% load widget_tweaks %} - {% block content %}
    -
    + {% csrf_token %}
    {{ form.name.label_tag }} {{ form.name|add_class:"form-control" }} - {% if form.name.help_text %} - {{ form.name.help_text }} - {% endif %} - {% for error in form.name.errors %} -
    {{ error }}
    - {% endfor %} + {% if form.name.help_text %}{{ form.name.help_text }}{% endif %} + {% for error in form.name.errors %}
    {{ error }}
    {% endfor %}
    {{ form.description.label_tag }} @@ -26,19 +23,13 @@ {% if form.description.help_text %} {{ form.description.help_text }} {% endif %} - {% for error in form.description.errors %} -
    {{ error }}
    - {% endfor %} + {% for error in form.description.errors %}
    {{ error }}
    {% endfor %}
    - -
    - + - {% trans 'Back'%} + {% trans 'Back' %}
    @@ -46,4 +37,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/chart_of_accounts/includes/coa_card.html b/templates/chart_of_accounts/includes/coa_card.html index 54d1f817..6240d177 100644 --- a/templates/chart_of_accounts/includes/coa_card.html +++ b/templates/chart_of_accounts/includes/coa_card.html @@ -1,7 +1,6 @@ {% load django_ledger %} {% load i18n %} {% now "Y" as current_year %} -
    @@ -19,17 +18,16 @@ {% endif %}
    - {% if coa_model.is_active %} + {% if coa_model.is_active %} {% trans 'Active' %} - {% else %} + {% else %} {% trans 'Inactive' %} - {% endif %} - {% if coa_model.is_default %} - {% trans 'Entity Default' %} - {% endif %} + {% endif %} + {% if coa_model.is_default %} + {% trans 'Entity Default' %} + {% endif %}
    -
    @@ -46,27 +44,22 @@ {{ coa_model.slug }}
    -
    {% trans 'Total Accounts' %}: {{ coa_model.accountmodel_total__count }}
    -
    {% trans 'Active Accounts' %}: {{ coa_model.accountmodel_active__count }}
    -
    {% trans 'Locked Accounts' %}: {{ coa_model.accountmodel_locked__count }}
    -
    -
    @@ -84,36 +77,37 @@
    - -
    \ No newline at end of file +
    diff --git a/templates/components/email_modal.html b/templates/components/email_modal.html index 47a1aa41..f65ae78b 100644 --- a/templates/components/email_modal.html +++ b/templates/components/email_modal.html @@ -1,16 +1,22 @@ {% load i18n crispy_forms_tags %} -
    -{% endblock customJS %} + + {% endblock customJS %} diff --git a/templates/crm/leads/lead_form.html b/templates/crm/leads/lead_form.html index 156527cc..9f5a75bd 100644 --- a/templates/crm/leads/lead_form.html +++ b/templates/crm/leads/lead_form.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% load i18n static crispy_forms_filters %} - {% block title %} {% if object %} {% trans 'Update Lead' %} @@ -8,7 +7,6 @@ {% trans 'Add New Lead' %} {% endif %} {% endblock %} - {% block customcss %} {% endblock customcss %} - {% block content %} -
    -
    -
    -
    -

    - {% if object %} - {% trans "Update Lead" %} - - {% else %} - {% trans "Create New Lead" %} - - {% endif %} -

    -
    -
    - - {% csrf_token %} - {{ form|crispy }} - -
    -
    - - - - {% trans "Cancel" %} - -
    - +
    +
    +
    +
    +

    + {% if object %} + {% trans "Update Lead" %} + + {% else %} + {% trans "Create New Lead" %} + + {% endif %} +

    +
    +
    +
    + {% csrf_token %} + {{ form|crispy }} +
    +
    + + + + {% trans "Cancel" %} + +
    +
    +
    -
    -
    -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html index c8ca9ea2..3c8757c7 100644 --- a/templates/crm/leads/lead_list.html +++ b/templates/crm/leads/lead_list.html @@ -1,270 +1,263 @@ - {% extends 'base.html' %} {% load i18n static humanize %} {% block title %} {{ _("Leads") |capfirst }} {% endblock title %} {% block content %} - -{% if page_obj.object_list or request.GET.q%} -
    -

    - {{ _("Leads") |capfirst }} -
  • -

    - - {% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %} - -
    -
    -
    - {% if perms.inventory.add_lead %} - - {% endif %} + {% if page_obj.object_list or request.GET.q %} +
    +

    + {{ _("Leads") |capfirst }} +
  • +

    + + {% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %} +
    +
    +
    + {% if perms.inventory.add_lead %} + + {% endif %} +
    +
    +
    +
    {% include 'partials/search_box.html' %}
    -
    -
    {% include 'partials/search_box.html' %}
    -
    -
    - -
    -
    - {% if page_obj.object_list or request.GET.q%} -
    - - - - - + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {{ _("Lead Name") |capfirst }} -
    -
    - +
    +
    + {% if page_obj.object_list or request.GET.q %} +
    + + + + + - + - + - + - + - + - {% comment %} + {% comment %} {% endcomment %} - - - - {% for lead in leads %} - - + + + {% for lead in leads %} + + - - - - - - - - - - - - + + - - {% empty %} - - - - {% endfor %} - -
    {{ _("Lead Name") |capfirst }} +
    +
    + +
    + {{ _("Car") |capfirst }}
    - {{ _("Car") |capfirst }} - -
    -
    -
    - +
    +
    +
    + +
    + {{ _("email") |capfirst }}
    - {{ _("email") |capfirst }} - -
    -
    -
    - +
    +
    +
    + +
    +
    {{ _("Phone Number") }}
    -
    {{ _("Phone Number") }}
    - -
    -
    -
    - +
    +
    +
    + +
    + {{ _("Next Action") |capfirst }}
    - {{ _("Next Action") |capfirst }} - -
    -
    -
    - +
    +
    +
    + +
    + {{ _("Scheduled at") }}
    - {{ _("Scheduled at") }} - -
    -
    -
    - +
    +
    +
    + +
    + {{ _("Assigned To") |capfirst }}
    - {{ _("Assigned To") |capfirst }} - -
    +
    {{ _("Opportunity")|capfirst }}
    -
    {{ _("Action") }}
    {{ _("Action") }}
    -
    -
    - {{ lead.full_name|capfirst }} -
    -

    - {% if lead.status == "new" %} - {{ _("New") }} - {% elif lead.status == "pending" %} - {{ _("Pending") }} - {% elif lead.status == "in_progress" %} - {{ _("In Progress") }} - {% elif lead.status == "qualified" %} - {{ _("Qualified") }} - {% elif lead.status == "contacted" %} - {{ _("Contacted") }} - {% elif lead.status == "canceled" %} - {{ _("Canceled") }} - {% endif %} -
    -
    -
    -
    - {{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }} - - {{ lead.email }} - - {{ lead.phone_number }} - - {{ lead.next_action|upper }} - {{ lead.next_action_date|upper }} -
    -
    - {% if lead.staff.logo %} - Logo - {% endif %} -
    - - {% if lead.staff == request.staff %} - {{ _("Me") }} - {% elif LANGUAGE_CODE == "en" %} - {{ lead.staff.fullname|capfirst }} - {% else %} - {{ lead.staff.arabic_name }} - {% endif %} - -
    -
    - {% if user == lead.staff.user or request.is_dealer %} -
    - -
    +
    +
    + {{ lead.full_name|capfirst }} +
    +

    + {% if lead.status == "new" %} + {{ _("New") }} + {% elif lead.status == "pending" %} + {{ _("Pending") }} + {% elif lead.status == "in_progress" %} + {{ _("In Progress") }} + {% elif lead.status == "qualified" %} + {{ _("Qualified") }} + {% elif lead.status == "contacted" %} + {{ _("Contacted") }} + {% elif lead.status == "canceled" %} + {{ _("Canceled") }} {% endif %} - {% endif %} - {% if perms.inventory.delete_lead %} - - - {% endif %} +
    - {% endif %} -
    {% trans "No Leads found." %}
    -
    - {% if page_obj.paginator.num_pages > 1 %} -
    -
    {% include 'partials/pagination.html' %}
    + +
    + {{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }} + + {{ lead.email }} + + {{ lead.phone_number }} + + {{ lead.next_action|upper }} + {{ lead.next_action_date|upper }} +
    +
    + {% if lead.staff.logo %} + Logo + {% endif %} +
    + + {% if lead.staff == request.staff %} + {{ _("Me") }} + {% elif LANGUAGE_CODE == "en" %} + {{ lead.staff.fullname|capfirst }} + {% else %} + {{ lead.staff.arabic_name }} + {% endif %} + +
    +
    + {% if user == lead.staff.user or request.is_dealer %} +
    + + +
    + {% endif %} +
    {% trans "No Leads found." %}
    + {% if page_obj.paginator.num_pages > 1 %} +
    +
    {% include 'partials/pagination.html' %}
    +
    + {% endif %} {% endif %} - - {% endif %} +
    -
    -{% else %} -{% url 'lead_create' request.dealer.slug as create_lead_url %} -{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} -{% endif %} -{% endblock %} + {% else %} + {% url 'lead_create' request.dealer.slug as create_lead_url %} + {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} + {% endif %} + {% endblock %} diff --git a/templates/crm/leads/lead_tracking.html b/templates/crm/leads/lead_tracking.html index bc93cb84..b3be0f19 100644 --- a/templates/crm/leads/lead_tracking.html +++ b/templates/crm/leads/lead_tracking.html @@ -5,179 +5,176 @@ {% endblock title %} {% block customCSS %} {% endblock customCSS %} {% block content %} - -{% if leads %} -
    -
    -
    -
    -

    - {{ _("Lead Tracking") }} -
  • -

    -
    -
    - -
    -
    -
    - {{ _("New Leads") }} ({{ new|length }}) + {% if leads %} +
    +
    +
    +
    +

    + {{ _("Lead Tracking") }} +
  • +

    +
    +
    + +
    +
    +
    + {{ _("New Leads") }} ({{ new|length }}) +
    + {% for lead in new %} + +
    + {{ lead.full_name|capfirst }} +
    + {{ lead.email }} +
    + {{ lead.phone_number }} +
    +
    + {% endfor %}
    - {% for lead in new %} - -
    - {{ lead.full_name|capfirst }} -
    - {{ lead.email }} -
    - {{ lead.phone_number }} -
    -
    - {% endfor %}
    -
    - -
    -
    -
    - {{ _("Follow Ups") }} ({{ follow_up|length }}) + +
    +
    +
    + {{ _("Follow Ups") }} ({{ follow_up|length }}) +
    + {% for lead in follow_up %} + +
    + {{ lead.full_name|capfirst }} +
    + {{ lead.email }} +
    + {{ lead.phone_number }} +
    +
    + {% endfor %}
    - {% for lead in follow_up %} - -
    - {{ lead.full_name|capfirst }} -
    - {{ lead.email }} -
    - {{ lead.phone_number }} -
    -
    - {% endfor %}
    -
    - -
    -
    -
    - {{ _("Negotiation Ups") }} ({{ follow_up|length }}) + +
    +
    +
    + {{ _("Negotiation Ups") }} ({{ follow_up|length }}) +
    + {% for lead in negotiation %} + +
    + {{ lead.full_name|capfirst }} +
    + {{ lead.email }} +
    + {{ lead.phone_number }} +
    +
    + {% endfor %}
    - {% for lead in negotiation %} - -
    - {{ lead.full_name|capfirst }} -
    - {{ lead.email }} -
    - {{ lead.phone_number }} -
    -
    - {% endfor %}
    -
    - -
    -
    -
    - {{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }}) + +
    +
    +
    + {{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }}) +
    + {% for lead in won %} + +
    + {{ lead.full_name|capfirst }} +
    + {{ lead.email }} +
    + {{ lead.phone_number }} +
    +
    + {% endfor %} +
    +
    + +
    +
    +
    {{ _("Lost") }} ({{ lose|length }})
    + {% for lead in lose %} + +
    + {{ lead.full_name|capfirst }} +
    + {{ lead.email }} +
    + {{ lead.phone_number }} +
    +
    + {% endfor %}
    - {% for lead in won %} - -
    - {{ lead.full_name|capfirst }} -
    - {{ lead.email }} -
    - {{ lead.phone_number }} -
    -
    - {% endfor %}
    - -
    -
    -
    {{ _("Lost") }} ({{ lose|length }})
    - {% for lead in lose %} - -
    - {{ lead.full_name|capfirst }} -
    - {{ lead.email }} -
    - {{ lead.phone_number }} -
    -
    - {% endfor %} -
    -
    - -
    -
    - {% else %} - {% url 'lead_create' request.dealer.slug as create_lead_url %} - {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} + {% else %} + {% url 'lead_create' request.dealer.slug as create_lead_url %} + {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} {% endif %} {% endblock %} diff --git a/templates/crm/leads/lead_view.html b/templates/crm/leads/lead_view.html index 4c6a367d..b96b5a3d 100644 --- a/templates/crm/leads/lead_view.html +++ b/templates/crm/leads/lead_view.html @@ -2,22 +2,22 @@ {% load static i18n humanize %} {% block customCSS %} {% endblock customCSS %} {% block content %} @@ -27,7 +27,7 @@
    مرحبًا
    + data-bs-toggle="dropdown">الصفحة الرئيسية لـ
    diff --git a/templates/crm/leads/partials/update_action.html b/templates/crm/leads/partials/update_action.html index fd5c99b2..c0163616 100644 --- a/templates/crm/leads/partials/update_action.html +++ b/templates/crm/leads/partials/update_action.html @@ -13,14 +13,11 @@ aria-label="Close">
    + action="{% url 'update_lead_actions' request.dealer.slug %}" + hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML" + hx-swap="none" + hx-on::after-request="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }" + method="post">