diff --git a/inventory/management/commands/test.py b/inventory/management/commands/test.py index e16ba9a2..52a7e369 100644 --- a/inventory/management/commands/test.py +++ b/inventory/management/commands/test.py @@ -1,21 +1,7 @@ from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from inventory.tasks import create_coa_accounts -from inventory.models import Dealer - -User = get_user_model() - +from inventory.tasks import long_running_task +from django_q.tasks import async_task class Command(BaseCommand): def handle(self, *args, **kwargs): - # user = User.objects.last() - # print(user.email) - # # 2. Force email confirmation - # # email = user.emailaddress_set.first() - # confirmation = EmailConfirmation.create(user.email) - # confirmation.send() - - # result = re.match(r'^05\d{8}$', '0625252522') - # print(result) - dealer = Dealer.objects.last() - create_coa_accounts(dealer.pk) + async_task(long_running_task, 20) \ No newline at end of file diff --git a/inventory/middleware.py b/inventory/middleware.py index 56e1993f..3990f4e2 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -126,7 +126,9 @@ class DealerSlugMiddleware: request.path_info.startswith('/en/login/') or \ request.path_info.startswith('/en/logout/') or \ request.path_info.startswith('/en/ledger/') or \ - request.path_info.startswith('/ar/ledger/'): + request.path_info.startswith('/en/ledger/') or \ + request.path_info.startswith('/en/notifications/') or \ + request.path_info.startswith('/ar/notifications/'): return None if not request.user.is_authenticated: @@ -141,6 +143,7 @@ class DealerSlugMiddleware: return None if dealer_slug.lower() != request.dealer.slug.lower(): + print(dealer_slug) logger.warning(f"Dealer slug mismatch: {dealer_slug} != {request.dealer.slug}") raise Http404("Dealer slug mismatch") diff --git a/inventory/models.py b/inventory/models.py index 00111089..2e1fea2a 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -3,6 +3,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.models import Permission from decimal import Decimal +from django.urls import reverse from django.utils.text import slugify from django.utils import timezone from django.core.validators import MinValueValidator @@ -600,6 +601,8 @@ class Car(Base): ) # history = HistoricalRecords() + def get_absolute_url(self): + return reverse("car_detail", kwargs={"dealer_slug": self.dealer.slug,"slug": self.slug}) def save(self, *args, **kwargs): self.slug = slugify(self.vin) self.hash = self.get_hash diff --git a/inventory/signals.py b/inventory/signals.py index 20a42738..bfcca5b0 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -1,4 +1,6 @@ from decimal import Decimal + +from django.urls import reverse from inventory.tasks import create_coa_accounts, create_make_accounts from django.contrib.auth.models import Group from django.db.models.signals import post_save, post_delete @@ -18,7 +20,7 @@ from django_ledger.models import ( from . import models from django.utils.timezone import now from django.db import transaction - +from django_q.tasks import async_task User = get_user_model() @@ -143,7 +145,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): entity.create_uom(name=u[1], unit_abbr=u[0]) # Create COA accounts, background task - create_coa_accounts(instance.pk) + async_task(create_coa_accounts,instance.pk) # create_settings(instance.pk) # create_accounts_for_make(instance.pk) @@ -164,12 +166,12 @@ def create_dealer_groups(sender, instance, created, **kwargs): :param kwargs: Additional keyword arguments passed by the signal. :type kwargs: dict """ - group_names = ["Inventory", "Accountant", "Sales"] + group_names = ["Inventory", "Accountant", "Sales","Manager"] def create_groups(): for group_name in group_names: group, created = Group.objects.get_or_create( - name=f"{instance.pk}_{group_name}" + name=f"{instance.slug}_{group_name}" ) group_manager, created = models.CustomGroup.objects.get_or_create( name=group_name, dealer=instance, group=group @@ -896,7 +898,26 @@ def update_finance_cost(sender, instance, created, **kwargs): @receiver(post_save, sender=PurchaseOrderModel) def create_po_item_upload(sender,instance,created,**kwargs): - if instance.po_status == "fulfilled": - for item in instance.get_itemtxs_data()[0]: + if instance.po_status == "fulfilled": + for item in instance.get_itemtxs_data()[0]: dealer = models.Dealer.objects.get(entity=instance.entity) models.PoItemsUploaded.objects.create(dealer=dealer,po=instance, item=item, status="fulfilled") + + + +########################################################## +######################Notification######################## +########################################################## + +@receiver(post_save, sender=models.Car) +def car_created_notification(sender, instance, created, **kwargs): + if created: + accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email) + for accountant in accountants: + models.Notification.objects.create( + user=accountant, + message=f""" + New Car {instance.vin} has been added to dealer {instance.dealer.name}. + View + """, + ) \ No newline at end of file diff --git a/inventory/tasks.py b/inventory/tasks.py index c04577ec..452c28fb 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -1,13 +1,11 @@ -from datetime import datetime from django.db import transaction from django_ledger.io import roles from django.core.mail import send_mail -from background_task import background from django.utils.translation import gettext_lazy as _ from inventory.models import DealerSettings, Dealer +from django_q.tasks import async_task -# @background def create_settings(pk): instance = Dealer.objects.get(pk=pk) @@ -34,7 +32,6 @@ def create_settings(pk): ) -@background def create_coa_accounts(pk): with transaction.atomic(): instance = Dealer.objects.select_for_update().get(pk=pk) @@ -772,8 +769,6 @@ def create_coa_accounts(pk): except Exception as e: print(e) - -@background def create_coa_accounts1(pk): with transaction.atomic(): instance = Dealer.objects.select_for_update().get(pk=pk) @@ -1456,25 +1451,16 @@ def create_make_accounts(entity, coa, makes, name, role, balance_type): ) return acc - -@background def send_email(from_, to_, subject, message): subject = subject message = message from_email = from_ recipient_list = [to_] - send_mail(subject, message, from_email, recipient_list) + async_task(send_mail,subject, message, from_email, recipient_list) -@background -def long_running_task(task_id, *args, **kwargs): +# @background +def long_running_task(duration): """Example background task""" - print(f"Starting task {task_id} with args: {args}, kwargs: {kwargs}") - - # Simulate work - for i in range(5): - print(f"Task {task_id} progress: {i + 1}/5") - - result = f"Task {task_id} completed at {datetime.now()}" - print(result) - return result + print("Task completed") + return True diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 031f26de..f8cbea70 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -644,4 +644,4 @@ def inventory_table(context, queryset): "inventory_list": queryset, } ctx.update(queryset.aggregate(inventory_total_value=Sum("total_value"))) - return ctx + return ctx \ No newline at end of file diff --git a/inventory/urls.py b/inventory/urls.py index f8f2a179..2e95003c 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -14,7 +14,6 @@ urlpatterns = [ path("/", views.HomeView.as_view(), name="home"), # Tasks - path("/tasks/", views.task_list, name="task_list"), path("legal/", views.terms_and_privacy, name="terms_and_privacy"), # path('tasks//detail/', views.task_detail, name='task_detail'), # Dashboards @@ -203,38 +202,29 @@ urlpatterns = [ ), # path('crm/opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), # ####################### - path("stream/", views.sse_stream, name="sse_stream"), - path("fetch/", views.fetch_notifications, name="fetch_notifications"), - # Mark single notification as read - path( - "/mark-read/", - views.mark_notification_as_read, - name="mark_notification_as_read", - ), - # Mark all notifications as read - path( - "mark-all-read/", - views.mark_all_notifications_as_read, - name="mark_all_notifications_as_read", - ), - # Notification history - path("history/", views.notifications_history, name="notifications_history"), # ####################### + # Notifications + path("notifications/stream/", views.sse_stream, name="sse_stream"), + path("notifications/fetch/", views.fetch_notifications, name="fetch_notifications"), + path( - "crm/notifications/", + "/notifications/", views.NotificationListView.as_view(), name="notifications_history", ), + path( - "crm/fetch_notifications/", - views.fetch_notifications, - name="fetch_notifications", - ), - path( - "crm/notifications//mark_as_read/", + "notifications//mark_as_read/", views.mark_notification_as_read, name="mark_notification_as_read", ), + path( + "notifications/mark_all_notifications_as_read/", + views.mark_all_notifications_as_read, + name="mark_all_notifications_as_read", + ), + # ####################### + # ####################### path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"), ####################################################### # Vendor URLs diff --git a/inventory/utils.py b/inventory/utils.py index d396fcf2..5790f2f4 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -26,7 +26,7 @@ from django_ledger.models import ( from django.utils.translation import get_language from appointment.models import StaffMember from django.contrib.auth.models import User - +from django_q.tasks import async_task import secrets @@ -152,7 +152,7 @@ def send_email(from_, to_, subject, message): message = message from_email = from_ recipient_list = [to_] - send_mail(subject, message, from_email, recipient_list) + async_task(send_mail,subject, message, from_email, recipient_list) def get_user_type(request): diff --git a/inventory/views.py b/inventory/views.py index fe5dd7b3..40693d5b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -23,7 +23,6 @@ from urllib.parse import urlparse, urlunparse from inventory.mixins import DealerSlugMixin from inventory.models import Status as LeadStatus from django.db import IntegrityError -from background_task.models import Task from django.views.generic import FormView from django.views.decorators.http import require_http_methods from django.db.models.deletion import RestrictedError @@ -2670,9 +2669,17 @@ class GroupCreateView( def form_valid(self, form): dealer = get_user_type(self.request) instance = form.save(commit=False) - group = Group.objects.create(name=f"{dealer.slug}_{instance.name}") - instance.dealer = dealer - instance.group = group + group_name = f"{dealer.slug}_{instance.name}" + group,created = Group.objects.get_or_create(name=group_name) + if created: + group_manager, created = models.CustomGroup.objects.get_or_create( + name=group_name, dealer=dealer, group=group + ) + group_manager.set_default_permissions() + dealer.user.groups.add(group) + else: + instance.dealer = dealer + instance.group = group instance.save() return super().form_valid(form) @@ -2951,10 +2958,10 @@ class UserCreateView( staff.staff_member = staff_member staff.dealer = dealer staff.add_as_manager() - group = Group.objects.filter(customgroup__name__iexact=staff.staff_type).first() + group = models.CustomGroup.objects.filter(dealer=dealer,name__iexact=staff.staff_type).first() staff.save() if group: - staff.add_group(group) + staff.add_group(group.group) return super().form_valid(form) def get_success_url(self): return reverse_lazy("user_list", args=[self.request.dealer.slug]) @@ -8777,20 +8784,8 @@ def payment_callback(request,dealer_slug): return render(request, "payment_failed.html", {"message": message}) -# Background Tasks -def task_list(request): - # Get all tasks ordered by creation time - tasks = Task.objects.all() - - # Add pagination - paginator = Paginator(tasks, 10) # Show 10 tasks per page - page_number = request.GET.get("page") - page_obj = paginator.get_page(page_number) - - return render(request, "tasks/task_list.html", {"page_obj": page_obj}) - - def sse_stream(request): + print("hi") def event_stream(): last_id = request.GET.get("last_id", 0) while True: @@ -8833,7 +8828,7 @@ def mark_notification_as_read(request, notification_id): notification = get_object_or_404( models.Notification, id=notification_id, user=request.user ) - notification.read = True + notification.is_read = True notification.save() return JsonResponse({"status": "success"}) @@ -8841,9 +8836,9 @@ def mark_notification_as_read(request, notification_id): @login_required def mark_all_notifications_as_read(request): models.Notification.objects.filter(user=request.user, is_read=False).update( - read=True + is_read=True ) - return JsonResponse({"status": "success"}) + return redirect(request.META.get("HTTP_REFERER")) @login_required @@ -9669,7 +9664,7 @@ def upload_cars(request, dealer_slug, pk=None): if not csv_file.name.endswith(".csv"): messages.error(request, "Please upload a CSV file") - return redirect("upload_cars", dealer_slug=dealer_slug) + return response try: # Read the file content file_content = csv_file.read().decode("utf-8") @@ -9715,10 +9710,16 @@ def upload_cars(request, dealer_slug, pk=None): po_item.save() messages.success(request, f"Successfully imported {cars_created} cars") - return response + return redirect( + "view_items_inventory", + dealer_slug=dealer_slug, + slug_entity=dealer.entity.slug, + po_pk=item.po_model.pk,) except Exception as e: messages.error(request, f"Error processing CSV: {str(e)}") + return response + form = forms.CSVUploadForm() form.fields["vendor"].queryset = dealer.vendors.all() diff --git a/requirements_dev.txt b/requirements_dev.txt index 841e904f..fb84285f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,18 +4,21 @@ arrow==1.3.0 asgiref==3.8.1 attrs==25.3.0 Babel==2.15.0 +beautifulsoup4==4.13.4 blessed==1.21.0 cattrs==24.1.3 certifi==2025.1.31 cffi==1.17.1 charset-normalizer==3.4.1 +click==8.2.1 colorama==0.4.6 crispy-bootstrap5==2024.10 cryptography==44.0.2 +cssbeautifier==1.15.4 defusedxml==0.7.1 diff-match-patch==20241021 distro==1.9.0 -Django==5.1.7 +Django==5.2.3 django-allauth==65.6.0 django-appointment==3.8.0 django-background-tasks==1.2.8 @@ -24,17 +27,20 @@ django-ckeditor==6.7.2 django-cors-headers==4.7.0 django-countries==7.6.1 django-crispy-forms==2.3 +django-easy-audit==1.3.7 django-extensions==3.2.3 django-filter==25.1 django-import-export==4.3.7 django-js-asset==3.1.2 -django-ledger==0.7.6.1 +django-ledger==0.7.7 +django-manager-utils==3.1.5 django-next-url-mixin==0.4.0 django-ordered-model==3.7.4 django-phonenumber-field==8.0.0 django-picklefield==3.3 django-plans==2.0.0 -django-q==1.3.9 +django-q2==1.8.0 +django-query-builder==3.2.0 django-schema-graph==3.1.0 django-sequences==3.0 django-tables2==2.7.5 @@ -42,29 +48,47 @@ django-treebeard==4.7.1 django-widget-tweaks==1.5.0 djangorestframework==3.15.2 djhtml==3.0.7 +djlint==1.36.4 docopt==0.6.2 -Faker==37.1.0 +EditorConfig==0.17.0 +Faker==37.3.0 +fleming==0.7.0 fonttools==4.57.0 fpdf==1.7.2 fpdf2==2.8.3 +greenlet==3.2.2 h11==0.14.0 httpcore==1.0.7 httpx==0.28.1 icalendar==6.1.2 idna==3.10 jiter==0.9.0 +jsbeautifier==1.15.4 +json5==0.12.0 +jsonpatch==1.33 +jsonpointer==3.0.0 jwt==1.3.1 +langchain==0.3.25 +langchain-core==0.3.61 +langchain-ollama==0.3.3 +langchain-text-splitters==0.3.8 +langsmith==0.3.42 luhnchecker==0.0.12 -Markdown==3.7 +Markdown==3.8 markdown-it-py==3.0.0 mdurl==0.1.2 num2words==0.5.14 numpy==2.2.4 ofxtools==0.9.5 +ollama==0.4.8 openai==1.68.2 opencv-python==4.11.0.86 +orjson==3.10.18 +packaging==24.2 +pandas==2.2.3 +pathspec==0.12.1 phonenumbers==8.13.42 -pillow==10.4.0 +pillow==11.2.1 pycparser==2.22 pydantic==2.10.6 pydantic_core==2.27.2 @@ -74,24 +98,29 @@ python-slugify==8.0.4 python-stdnum==1.20 pytz==2025.2 pyvin==0.0.2 +PyYAML==6.0.2 pyzbar==0.1.9 redis==3.5.3 +regex==2024.11.6 requests==2.32.3 +requests-toolbelt==1.0.0 rich==14.0.0 +ruff==0.11.10 setuptools==80.3.0 six==1.17.0 sniffio==1.3.1 +soupsieve==2.7 +SQLAlchemy==2.0.41 sqlparse==0.5.3 suds==1.2.0 swapper==1.3.0 tablib==3.8.0 +tenacity==9.1.2 text-unidecode==1.3 tqdm==4.67.1 -types-python-dateutil==2.9.0.20241206 +types-python-dateutil==2.9.0.20250516 typing_extensions==4.13.0 tzdata==2025.2 urllib3==2.3.0 wcwidth==0.2.13 -langchain -langchain_ollama -django-easy-audit==1.3.7 \ No newline at end of file +zstandard==0.23.0 diff --git a/templates/admin_management/user_management.html b/templates/admin_management/user_management.html index e9dd1b19..435039d2 100644 --- a/templates/admin_management/user_management.html +++ b/templates/admin_management/user_management.html @@ -243,7 +243,7 @@ diff --git a/templates/crm/notifications_history.html b/templates/crm/notifications_history.html index 804d010b..72dbb8df 100644 --- a/templates/crm/notifications_history.html +++ b/templates/crm/notifications_history.html @@ -4,7 +4,9 @@

{{ _("Notifications") }}

- + {% if notifications %}
{% for notification in notifications %} @@ -20,10 +22,7 @@

{{ notification.created }}

- + {% endfor %} diff --git a/templates/notifications.html b/templates/notifications.html index 74190fc0..216ae8f4 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -21,7 +21,7 @@
{{ _("Notifications") }}
- +
@@ -72,7 +72,7 @@ data.notifications.forEach(notification => { seenNotificationIds.add(notification.id); - if (notification.unread) { + if (!notification.is_read) { unreadCount++; } }); @@ -136,7 +136,7 @@ } function createNotificationElement(data) { - const isRead = data.read ? 'read' : 'unread'; + const isRead = data.is_read ? 'read' : 'unread'; return `
@@ -223,7 +223,7 @@ if (e.target.classList.contains('mark-as-read')) { e.preventDefault(); const notificationId = e.target.getAttribute('data-notification-id'); - fetch(`/notifications/${notificationId}/mark-read/`, { + fetch(`/notifications/${notificationId}/mark_as_read/`, { method: 'POST', headers: { 'X-CSRFToken': '{{ csrf_token }}', diff --git a/templates/purchase_orders/includes/card_po.html b/templates/purchase_orders/includes/card_po.html index 2ccd8d2f..95574c33 100644 --- a/templates/purchase_orders/includes/card_po.html +++ b/templates/purchase_orders/includes/card_po.html @@ -82,7 +82,7 @@
{% trans 'Purchase Order Amount' %}
-

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+

{{CURRENCY}}{{ po_model.po_amount|currency_format }}

@@ -101,7 +101,7 @@
{% trans 'Purchase Order Amount' %}
-

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+

{{CURRENCY}}{{ po_model.po_amount|currency_format }}

@@ -120,7 +120,7 @@
{% trans 'PO Amount' %}
-

{{ po_model.po_amount|currency_format }}{% currency_symbol %}

+

{{ po_model.po_amount|currency_format }}{{CURRENCY}}

@@ -128,7 +128,7 @@
{% trans 'Received Amount' %}
-

{{ po_model.po_amount_received|currency_format }}{% currency_symbol %}

+

{{ po_model.po_amount_received|currency_format }}{{CURRENCY}}

@@ -148,7 +148,7 @@
{% trans 'PO Amount' %}
-

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+

{{CURRENCY}}{{ po_model.po_amount|currency_format }}

{% trans 'Fulfilled' %} diff --git a/templates/purchase_orders/includes/po_item_formset.html b/templates/purchase_orders/includes/po_item_formset.html index 13ca5c3b..10e9e530 100644 --- a/templates/purchase_orders/includes/po_item_formset.html +++ b/templates/purchase_orders/includes/po_item_formset.html @@ -18,7 +18,7 @@ - + {{CURRENCY}}{{ f.instance.po_total_amount | currency_format }} {% if itemtxs_formset.can_delete %} - + {% if itemtxs_formset.can_delete %} diff --git a/templates/purchase_orders/includes/po_table.html b/templates/purchase_orders/includes/po_table.html index c651e0f6..4e7e9713 100644 --- a/templates/purchase_orders/includes/po_table.html +++ b/templates/purchase_orders/includes/po_table.html @@ -21,7 +21,7 @@ - + - + @@ -69,8 +69,8 @@ - - + + {% endfor %} diff --git a/templates/purchase_orders/tags/po_item_table.html b/templates/purchase_orders/tags/po_item_table.html index 42404421..c194ecad 100644 --- a/templates/purchase_orders/tags/po_item_table.html +++ b/templates/purchase_orders/tags/po_item_table.html @@ -20,7 +20,7 @@ + {{CURRENCY}}{{ item.po_total_amount | currency_format }} + {{CURRENCY}}{{ po_model.po_amount | currency_format }} diff --git a/templates/users/user_list.html b/templates/users/user_list.html index f0638508..40e01930 100644 --- a/templates/users/user_list.html +++ b/templates/users/user_list.html @@ -42,7 +42,6 @@
+ {% trans 'Item' %} {% if po_model.is_draft %} {{ f.entity_unit|add_class:"form-control" }} - {% currency_symbol %}{{ f.instance.po_total_amount | currency_format }} {{ f.po_item_status|add_class:"form-control" }} @@ -98,7 +98,7 @@ {% trans 'Total' %}{% currency_symbol %}{{ po_model.po_amount | currency_format }}{{CURRENCY}}{{ po_model.po_amount | currency_format }} {{ po.po_title }} {{ po.get_status_action_date }} {{ po.get_po_status_display }}{% currency_symbol %}{{ po.po_amount | currency_format }}{{CURRENCY}}{{ po.po_amount | currency_format }} + -
+
{% po_item_table1 po_items %}
- -
+ + {% include "purchase_orders/includes/mark_as.html" %} diff --git a/templates/purchase_orders/po_update.html b/templates/purchase_orders/po_update.html index 8f8aa474..0e77c4a7 100644 --- a/templates/purchase_orders/po_update.html +++ b/templates/purchase_orders/po_update.html @@ -61,7 +61,7 @@
{% currency_symbol %}{{ ce_cost_estimate__sum | currency_format }}{{CURRENCY}}{{ ce_cost_estimate__sum | currency_format }}
{{ i.item_model__name }} {{ i.ce_quantity__sum }}{% currency_symbol %}{{ i.avg_unit_cost | currency_format }}{% currency_symbol %}{{ i.ce_cost_estimate__sum | currency_format }}{{CURRENCY}}{{ i.avg_unit_cost | currency_format }}{{CURRENCY}}{{ i.ce_cost_estimate__sum | currency_format }}
{{ item.po_unit_cost }} {{ item.po_quantity }} - {% currency_symbol %}{{ item.po_total_amount | currency_format }} {% if item.po_item_status %} {{ item.get_po_item_status_display }} @@ -40,7 +40,7 @@ {% trans 'Total PO Amount' %} - {% currency_symbol %}{{ po_model.po_amount | currency_format }}
{{ user.arabic_name }} - {{user.dealer}}
{{ user.email }}