Compare commits

...

15 Commits

35 changed files with 405 additions and 273 deletions

View File

@ -1,21 +1,7 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from inventory.tasks import long_running_task
from inventory.tasks import create_coa_accounts from django_q.tasks import async_task
from inventory.models import Dealer
User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# user = User.objects.last() async_task(long_running_task, 20)
# 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)

View File

@ -10,38 +10,38 @@ from inventory.utils import get_user_type
logger = logging.getLogger("user_activity") logger = logging.getLogger("user_activity")
class LogUserActivityMiddleware: # class LogUserActivityMiddleware:
""" # """
Middleware for logging user activity. # Middleware for logging user activity.
This middleware logs the activity of authenticated users each time they make a # This middleware logs the activity of authenticated users each time they make a
request. It creates an entry in the UserActivityLog model capturing the user's # request. It creates an entry in the UserActivityLog model capturing the user's
ID, the action performed, and the timestamp. It is intended to assist in # ID, the action performed, and the timestamp. It is intended to assist in
tracking user actions across the application for analytics or auditing purposes. # tracking user actions across the application for analytics or auditing purposes.
:ivar get_response: The next middleware or view in the WSGI request-response # :ivar get_response: The next middleware or view in the WSGI request-response
chain. # chain.
:type get_response: Callable # :type get_response: Callable
""" # """
def __init__(self, get_response): # def __init__(self, get_response):
self.get_response = get_response # self.get_response = get_response
def __call__(self, request): # def __call__(self, request):
response = self.get_response(request) # response = self.get_response(request)
if request.user.is_authenticated: # if request.user.is_authenticated:
action = f"{request.method} {request.path}" # action = f"{request.method} {request.path}"
models.UserActivityLog.objects.create( # models.UserActivityLog.objects.create(
user=request.user, action=action, timestamp=timezone.now() # user=request.user, action=action, timestamp=timezone.now()
) # )
return response # return response
def get_client_ip(self, request): # def get_client_ip(self, request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") # x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: # if x_forwarded_for:
return x_forwarded_for.split(",")[0] # return x_forwarded_for.split(",")[0]
return request.META.get("REMOTE_ADDR") # return request.META.get("REMOTE_ADDR")
class InjectParamsMiddleware: class InjectParamsMiddleware:
@ -93,12 +93,13 @@ class InjectDealerMiddleware:
def __call__(self, request): def __call__(self, request):
try: try:
request.is_dealer = False if request.user.is_authenticated:
request.is_staff = False request.is_dealer = False
if hasattr(request.user, "dealer"): request.is_staff = False
request.is_dealer = True if hasattr(request.user, "dealer"):
elif hasattr(request.user, "staffmember"): request.is_dealer = True
request.is_staff = True elif hasattr(request.user, "staffmember"):
request.is_staff = True
except Exception: except Exception:
pass pass
response = self.get_response(request) response = self.get_response(request)
@ -120,14 +121,30 @@ class DealerSlugMiddleware:
def __call__(self, request): def __call__(self, request):
response = self.get_response(request) response = self.get_response(request)
return response return response
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
if request.path_info.startswith('/en/signup/') or \
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('/en/ledger/') or \
request.path_info.startswith('/en/notifications/') or \
request.path_info.startswith('/ar/notifications/'):
return None
if not request.user.is_authenticated: if not request.user.is_authenticated:
return None return None
if request.path.startswith('/en/ledger/') or request.path.startswith('/ar/ledger/'):
dealer_slug = view_kwargs.get("dealer_slug")
if not dealer_slug:
return None return None
if not view_kwargs.get("dealer_slug"):
if not hasattr(request, 'dealer') or not request.dealer:
logger.warning("No dealer associated with request")
return None return None
dealer = get_user_type(request)
if view_kwargs["dealer_slug"] != dealer.slug: 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") raise Http404("Dealer slug mismatch")
return None

View File

@ -3,6 +3,7 @@ from datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from decimal import Decimal from decimal import Decimal
from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils import timezone from django.utils import timezone
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -600,6 +601,8 @@ class Car(Base):
) )
# history = HistoricalRecords() # 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): def save(self, *args, **kwargs):
self.slug = slugify(self.vin) self.slug = slugify(self.vin)
self.hash = self.get_hash self.hash = self.get_hash
@ -2199,6 +2202,9 @@ class Vendor(models.Model, LocalizedNameMixin):
max_length=255, unique=True, verbose_name=_("Slug"), null=True, blank=True max_length=255, unique=True, verbose_name=_("Slug"), null=True, blank=True
) )
def get_absolute_url(self):
return reverse("vendor_detail", kwargs={"dealer_slug":self.dealer.slug,"slug": self.slug})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:
base_slug = slugify(self.name) base_slug = slugify(self.name)

View File

@ -1,4 +1,6 @@
from decimal import Decimal from decimal import Decimal
from django.urls import reverse
from inventory.tasks import create_coa_accounts, create_make_accounts from inventory.tasks import create_coa_accounts, create_make_accounts
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
@ -18,7 +20,7 @@ from django_ledger.models import (
from . import models from . import models
from django.utils.timezone import now from django.utils.timezone import now
from django.db import transaction from django.db import transaction
from django_q.tasks import async_task
User = get_user_model() 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]) entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task # Create COA accounts, background task
create_coa_accounts(instance.pk) async_task(create_coa_accounts,instance.pk)
# create_settings(instance.pk) # create_settings(instance.pk)
# create_accounts_for_make(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. :param kwargs: Additional keyword arguments passed by the signal.
:type kwargs: dict :type kwargs: dict
""" """
group_names = ["Inventory", "Accountant", "Sales"] group_names = ["Inventory", "Accountant", "Sales","Manager"]
def create_groups(): def create_groups():
for group_name in group_names: for group_name in group_names:
group, created = Group.objects.get_or_create( 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( group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=instance, group=group name=group_name, dealer=instance, group=group
@ -896,7 +898,86 @@ def update_finance_cost(sender, instance, created, **kwargs):
@receiver(post_save, sender=PurchaseOrderModel) @receiver(post_save, sender=PurchaseOrderModel)
def create_po_item_upload(sender,instance,created,**kwargs): def create_po_item_upload(sender,instance,created,**kwargs):
if instance.po_status == "fulfilled": if instance.po_status == "fulfilled":
for item in instance.get_itemtxs_data()[0]: for item in instance.get_itemtxs_data()[0]:
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
models.PoItemsUploaded.objects.create(dealer=dealer,po=instance, item=item, status="fulfilled") 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}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=PurchaseOrderModel)
def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled():
dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all()
for recipient in recipients:
models.Notification.objects.create(
user=recipient,
message=f"""
New Purchase Order has been added.
<a href="{reverse('purchase_order_detail',kwargs={'dealer_slug':dealer.slug,'pk':instance.pk})}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=models.Vendor)
def vendor_created_notification(sender, instance, created, **kwargs):
if created:
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all()
for recipient in recipients:
models.Notification.objects.create(
user=recipient,
message=f"""
New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=models.SaleOrder)
def sale_order_created_notification(sender, instance, created, **kwargs):
if created:
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email)
for recipient in recipients:
models.Notification.objects.create(
user=recipient,
message=f"""
New Sale Order has been added for estimate:{instance.estimate}.
<a href="{reverse('estimate_detail',kwargs={'dealer_slug':instance.dealer.slug,'pk':instance.pk})}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=models.Lead)
def lead_created_notification(sender, instance, created, **kwargs):
if created:
models.Notification.objects.create(
user=instance.staff.user,
message=f"""
New Lead has been added.
<a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=models.Lead)
def lead_created_notification(sender, instance, created, **kwargs):
if created:
models.Notification.objects.create(
user=instance.staff.user,
message=f"""
New Lead has been added.
<a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a>
""",
)

View File

@ -1,13 +1,11 @@
from datetime import datetime
from django.db import transaction from django.db import transaction
from django_ledger.io import roles from django_ledger.io import roles
from django.core.mail import send_mail from django.core.mail import send_mail
from background_task import background
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from inventory.models import DealerSettings, Dealer from inventory.models import DealerSettings, Dealer
from django_q.tasks import async_task
# @background
def create_settings(pk): def create_settings(pk):
instance = Dealer.objects.get(pk=pk) instance = Dealer.objects.get(pk=pk)
@ -34,7 +32,6 @@ def create_settings(pk):
) )
@background
def create_coa_accounts(pk): def create_coa_accounts(pk):
with transaction.atomic(): with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk) instance = Dealer.objects.select_for_update().get(pk=pk)
@ -772,8 +769,6 @@ def create_coa_accounts(pk):
except Exception as e: except Exception as e:
print(e) print(e)
@background
def create_coa_accounts1(pk): def create_coa_accounts1(pk):
with transaction.atomic(): with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk) 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 return acc
@background
def send_email(from_, to_, subject, message): def send_email(from_, to_, subject, message):
subject = subject subject = subject
message = message message = message
from_email = from_ from_email = from_
recipient_list = [to_] recipient_list = [to_]
send_mail(subject, message, from_email, recipient_list) async_task(send_mail,subject, message, from_email, recipient_list)
@background # @background
def long_running_task(task_id, *args, **kwargs): def long_running_task(duration):
"""Example background task""" """Example background task"""
print(f"Starting task {task_id} with args: {args}, kwargs: {kwargs}") print("Task completed")
return True
# 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

View File

@ -644,4 +644,4 @@ def inventory_table(context, queryset):
"inventory_list": queryset, "inventory_list": queryset,
} }
ctx.update(queryset.aggregate(inventory_total_value=Sum("total_value"))) ctx.update(queryset.aggregate(inventory_total_value=Sum("total_value")))
return ctx return ctx

View File

@ -8,44 +8,12 @@ from django.conf.urls import handler403, handler400, handler404, handler500
urlpatterns = [ urlpatterns = [
# main URLs # main URLs
path("", views.WelcomeView, name="welcome"),
path("signup/", views.dealer_signup, name="account_signup"),
path("", views.HomeView.as_view(), name="home"), path("", views.HomeView.as_view(), name="home"),
path("<slug:dealer_slug>/", views.HomeView.as_view(), name="home"), path("<slug:dealer_slug>/", views.HomeView.as_view(), name="home"),
path("welcome/", views.WelcomeView.as_view(), name="welcome"),
# Accounts URLs
# path("login/", allauth_views.LoginView.as_view(template_name="account/login.html"), name="account_login"),
# path(
# "logout/",
# allauth_views.LogoutView.as_view(template_name="account/logout.html"),
# name="account_logout",
# ),
# path('signup/', allauth_views.SignupView.as_view(template_name='account/signup.html'), name='account_signup'),
path("signup/", views.dealer_signup, name="account_signup"),
# path("otp", views.OTPView.as_view(), name="otp"),
# path(
# "password/change/", allauth_views.PasswordChangeView.as_view(template_name="account/password_change.html"), name="account_change_password",
# ),
# path(
# "password/reset/",
# allauth_views.PasswordResetView.as_view(
# template_name="account/password_reset.html"
# ),
# name="account_reset_password",
# ),
# path(
# "accounts/password/reset/done/",
# allauth_views.PasswordResetDoneView.as_view(
# template_name="account/password_reset_done.html"
# ),
# name="account_password_reset_done",
# ),
# path(
# "accounts/login/code/",
# allauth_views.RequestLoginCodeView.as_view(
# template_name="account/request_login_code.html"
# ),
# ),
# Tasks # Tasks
path("tasks/", views.task_list, name="task_list"),
path("legal/", views.terms_and_privacy, name="terms_and_privacy"), path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'), # path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards # Dashboards
@ -234,38 +202,29 @@ urlpatterns = [
), ),
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), # path('crm/opportunities/<int:pk>/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(
"<int:notification_id>/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( path(
"crm/notifications/", "notifications/",
views.NotificationListView.as_view(), views.NotificationListView.as_view(),
name="notifications_history", name="notifications_history",
), ),
path( path(
"crm/fetch_notifications/", "notifications/<int:notification_id>/mark_as_read/",
views.fetch_notifications,
name="fetch_notifications",
),
path(
"crm/notifications/<int:notification_id>/mark_as_read/",
views.mark_notification_as_read, views.mark_notification_as_read,
name="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"), path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"),
####################################################### #######################################################
# Vendor URLs # Vendor URLs

View File

@ -26,7 +26,7 @@ from django_ledger.models import (
from django.utils.translation import get_language from django.utils.translation import get_language
from appointment.models import StaffMember from appointment.models import StaffMember
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django_q.tasks import async_task
import secrets import secrets
@ -152,7 +152,7 @@ def send_email(from_, to_, subject, message):
message = message message = message
from_email = from_ from_email = from_
recipient_list = [to_] 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): def get_user_type(request):

View File

@ -8,7 +8,6 @@ import logging
import tempfile import tempfile
import numpy as np import numpy as np
from time import sleep from time import sleep
# from rich import print # from rich import print
from random import randint from random import randint
from decimal import Decimal from decimal import Decimal
@ -23,7 +22,6 @@ from urllib.parse import urlparse, urlunparse
from inventory.mixins import DealerSlugMixin from inventory.mixins import DealerSlugMixin
from inventory.models import Status as LeadStatus from inventory.models import Status as LeadStatus
from django.db import IntegrityError from django.db import IntegrityError
from background_task.models import Task
from django.views.generic import FormView from django.views.generic import FormView
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.db.models.deletion import RestrictedError from django.db.models.deletion import RestrictedError
@ -268,8 +266,10 @@ def switch_language(request):
logger.warning(f"Invalid language code: {language}") logger.warning(f"Invalid language code: {language}")
return redirect("/") return redirect("/")
def testview(request):
return HttpResponse("test")
def dealer_signup(request, *args, **kwargs): def dealer_signup(request):
""" """
Handles the dealer signup wizard process, including forms validation, user and group Handles the dealer signup wizard process, including forms validation, user and group
creation, permissions assignment, and dealer data storage. This view supports GET creation, permissions assignment, and dealer data storage. This view supports GET
@ -557,7 +557,7 @@ def terms_and_privacy(request):
return render(request, "terms_and_privacy.html") return render(request, "terms_and_privacy.html")
class WelcomeView(TemplateView): def WelcomeView(request):
""" """
Handles the rendering and context data for the Welcome view. Handles the rendering and context data for the Welcome view.
@ -569,14 +569,11 @@ class WelcomeView(TemplateView):
:ivar template_name: Path to the template used by the view. :ivar template_name: Path to the template used by the view.
:type template_name: str :type template_name: str
""" """
if request.user.is_authenticated:
template_name = "welcome.html" return redirect("home", dealer_slug=request.dealer.slug)
plan_list = Plan.objects.all()
def get_context_data(self, **kwargs): context = {"plan_list": plan_list}
context = super().get_context_data(**kwargs) return render(request, "welcome.html", context)
plan_list = Plan.objects.all()
context["plan_list"] = plan_list
return context
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -1091,7 +1088,7 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"sold": cars.filter(status="sold").count(), "sold": cars.filter(status="sold").count(),
"transfer": cars.filter(status="transfer").count(), "transfer": cars.filter(status="transfer").count(),
} }
context["make"] = models.CarMake.objects.filter(car__in=cars).distinct() context["make"] = models.CarMake.objects.filter(is_sa_import=True,car__in=cars).distinct()
context["model"] = models.CarModel.objects.none() context["model"] = models.CarModel.objects.none()
context["year"] = models.Car.objects.none() context["year"] = models.Car.objects.none()
make = self.request.GET.get("make") make = self.request.GET.get("make")
@ -1573,7 +1570,7 @@ class CarDeleteView(
model = models.Car model = models.Car
template_name = "inventory/car_confirm_delete.html" template_name = "inventory/car_confirm_delete.html"
permission_required = ["inventory.delete_car"] permission_required = ["inventory.delete_car"]
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
@ -1584,14 +1581,14 @@ class CarDeleteView(
Returns the URL to redirect to after a successful car deletion. Returns the URL to redirect to after a successful car deletion.
It dynamically includes the dealer_slug from the URL. It dynamically includes the dealer_slug from the URL.
""" """
dealer_slug = self.kwargs.get('dealer_slug') dealer_slug = self.kwargs.get('dealer_slug')
if dealer_slug: if dealer_slug:
return reverse_lazy("car_list", kwargs={'dealer_slug': dealer_slug}) return reverse_lazy("car_list", kwargs={'dealer_slug': dealer_slug})
else: else:
messages.error(self.request, _("Could not determine dealer for redirection.")) messages.error(self.request, _("Could not determine dealer for redirection."))
return reverse_lazy("home") return reverse_lazy("home")
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -2068,7 +2065,7 @@ class DealerDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
dealer = self.object dealer = self.object
car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer) car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer,is_sa_import=True)
staff_count = dealer.staff_count staff_count = dealer.staff_count
cars_count = models.Car.objects.filter(dealer=dealer).count() cars_count = models.Car.objects.filter(dealer=dealer).count()
@ -2497,7 +2494,7 @@ class VendorCreateView(
) )
else: else:
messages.error(self.request, _("Vendor with this email already exists")) messages.error(self.request, _("Vendor with this email already exists"))
return redirect("vendor_create") return redirect("vendor_create",dealer_slug=self.kwargs["dealer_slug"])
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
form.instance.dealer = dealer form.instance.dealer = dealer
form.instance.save() form.instance.save()
@ -2671,9 +2668,17 @@ class GroupCreateView(
def form_valid(self, form): def form_valid(self, form):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
instance = form.save(commit=False) instance = form.save(commit=False)
group = Group.objects.create(name=f"{dealer.slug}_{instance.name}") group_name = f"{dealer.slug}_{instance.name}"
instance.dealer = dealer group,created = Group.objects.get_or_create(name=group_name)
instance.group = group 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() instance.save()
return super().form_valid(form) return super().form_valid(form)
@ -2952,10 +2957,10 @@ class UserCreateView(
staff.staff_member = staff_member staff.staff_member = staff_member
staff.dealer = dealer staff.dealer = dealer
staff.add_as_manager() 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() staff.save()
if group: if group:
staff.add_group(group) staff.add_group(group.group)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse_lazy("user_list", args=[self.request.dealer.slug]) return reverse_lazy("user_list", args=[self.request.dealer.slug])
@ -5610,6 +5615,10 @@ def lead_transfer(request,dealer_slug, slug):
if form.is_valid(): if form.is_valid():
lead.staff = form.cleaned_data["transfer_to"] lead.staff = form.cleaned_data["transfer_to"]
lead.save() lead.save()
models.Notification.objects.create(
user=lead.staff.user,
message=f"You have been assigned a new lead: {lead.full_name}.",
)
messages.success(request, _("Lead transferred successfully")) messages.success(request, _("Lead transferred successfully"))
else: else:
messages.error(request, f"Invalid form data: {str(form.errors)}") messages.error(request, f"Invalid form data: {str(form.errors)}")
@ -8778,20 +8787,8 @@ def payment_callback(request,dealer_slug):
return render(request, "payment_failed.html", {"message": message}) 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): def sse_stream(request):
print("hi")
def event_stream(): def event_stream():
last_id = request.GET.get("last_id", 0) last_id = request.GET.get("last_id", 0)
while True: while True:
@ -8834,7 +8831,7 @@ def mark_notification_as_read(request, notification_id):
notification = get_object_or_404( notification = get_object_or_404(
models.Notification, id=notification_id, user=request.user models.Notification, id=notification_id, user=request.user
) )
notification.read = True notification.is_read = True
notification.save() notification.save()
return JsonResponse({"status": "success"}) return JsonResponse({"status": "success"})
@ -8842,9 +8839,9 @@ def mark_notification_as_read(request, notification_id):
@login_required @login_required
def mark_all_notifications_as_read(request): def mark_all_notifications_as_read(request):
models.Notification.objects.filter(user=request.user, is_read=False).update( 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 @login_required
@ -9251,7 +9248,7 @@ def InventoryItemCreateView(request, dealer_slug):
form = forms.CSVUploadForm() form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.filter(active=True) form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
context = { context = {
"make_data": models.CarMake.objects.all(), "make_data": models.CarMake.objects.filter(is_sa_import=True),
"inventory_accounts": inventory_accounts, "inventory_accounts": inventory_accounts,
"cogs_accounts": cogs_accounts, "cogs_accounts": cogs_accounts,
"form": form, "form": form,
@ -9261,7 +9258,7 @@ def InventoryItemCreateView(request, dealer_slug):
request, request,
"purchase_orders/inventory_item_form.html", "purchase_orders/inventory_item_form.html",
{ {
"make_data": models.CarMake.objects.all(), "make_data": models.CarMake.objects.filter(is_sa_import=True),
"inventory_accounts": inventory_accounts, "inventory_accounts": inventory_accounts,
"cogs_accounts": cogs_accounts, "cogs_accounts": cogs_accounts,
}, },
@ -9325,6 +9322,74 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
entity = dealer.entity entity = dealer.entity
return self.model.objects.filter(entity=entity) return self.model.objects.filter(entity=entity)
def get_queryset(self):
dealer = get_user_type(self.request)
entity = dealer.entity
queryset = self.model.objects.filter(entity=entity)
query = self.request.GET.get('q') # This is generic: looks for 'q' from GET
if query:
# Start with an empty Q object for the search filters
search_filters = Q()
# 1. Try to parse the query as a date
parsed_date = None
date_formats = [
'%Y-%m-%d', # 2023-10-26
'%m/%d/%Y', # 10/26/2023
'%d-%m-%Y', # 26-10-2023
'%B %d, %Y', # October 26, 2023
'%b %d, %Y', # Oct 26, 2023
'%Y/%m/%d', # 2023/10/26
'%Y-%m', # 2023-10 (for year-month search)
'%Y',
'%b %d',
'%B %d' # 2023 (for year search)
]
for fmt in date_formats:
try:
# For '%Y-%m' and '%Y', we only care about year/month, not exact day
if fmt == '%Y-%m':
parsed_date = datetime.strptime(query, fmt)
search_filters |= Q(created__year=parsed_date.year, created__month=parsed_date.month)
break
elif fmt == '%Y':
parsed_date = datetime.strptime(query, fmt)
search_filters |= Q(created__year=parsed_date.year)
break
else:
parsed_date = datetime.strptime(query, fmt).date()
search_filters |= Q(created__date=parsed_date) # Matches exact date part of datetime field
break # Found a match, no need to try other formats
except ValueError:
continue # Try next format
# 2. Add text-based search filters (always apply these)
# Combine them with OR operator
text_filters = (
Q(po_number__icontains=query) |
Q(po_title__icontains=query) |
Q(po_status__icontains=query) |
Q(created__icontains=query)
)
# If a date was successfully parsed, combine with text filters
if parsed_date:
# Use a combined Q object. This means it will search for
# (date_match OR po_number_match OR po_title_match)
queryset = queryset.filter(search_filters | text_filters).distinct()
else:
# If no date was parsed, only apply text filters
queryset = queryset.filter(text_filters).distinct()
return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
@ -9535,7 +9600,7 @@ class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView): class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_fulfilled" action_name = "mark_as_fulfilled"
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView): class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_canceled" action_name = "mark_as_canceled"
@ -9670,7 +9735,7 @@ def upload_cars(request, dealer_slug, pk=None):
if not csv_file.name.endswith(".csv"): if not csv_file.name.endswith(".csv"):
messages.error(request, "Please upload a CSV file") messages.error(request, "Please upload a CSV file")
return redirect("upload_cars", dealer_slug=dealer_slug) return response
try: try:
# Read the file content # Read the file content
file_content = csv_file.read().decode("utf-8") file_content = csv_file.read().decode("utf-8")
@ -9716,17 +9781,23 @@ def upload_cars(request, dealer_slug, pk=None):
po_item.save() po_item.save()
messages.success(request, f"Successfully imported {cars_created} cars") 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: except Exception as e:
messages.error(request, f"Error processing CSV: {str(e)}") messages.error(request, f"Error processing CSV: {str(e)}")
return response
form = forms.CSVUploadForm() form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.all() form.fields["vendor"].queryset = dealer.vendors.all()
return render( return render(
request, request,
"csv_upload.html", "csv_upload.html",
{"make_data": models.CarMake.objects.all(), "form": form, "item": item}, {"make_data": models.CarMake.objects.filter(is_sa_import=True), "form": form, "item": item},
) )

View File

@ -4,18 +4,21 @@ arrow==1.3.0
asgiref==3.8.1 asgiref==3.8.1
attrs==25.3.0 attrs==25.3.0
Babel==2.15.0 Babel==2.15.0
beautifulsoup4==4.13.4
blessed==1.21.0 blessed==1.21.0
cattrs==24.1.3 cattrs==24.1.3
certifi==2025.1.31 certifi==2025.1.31
cffi==1.17.1 cffi==1.17.1
charset-normalizer==3.4.1 charset-normalizer==3.4.1
click==8.2.1
colorama==0.4.6 colorama==0.4.6
crispy-bootstrap5==2024.10 crispy-bootstrap5==2024.10
cryptography==44.0.2 cryptography==44.0.2
cssbeautifier==1.15.4
defusedxml==0.7.1 defusedxml==0.7.1
diff-match-patch==20241021 diff-match-patch==20241021
distro==1.9.0 distro==1.9.0
Django==5.1.7 Django==5.2.3
django-allauth==65.6.0 django-allauth==65.6.0
django-appointment==3.8.0 django-appointment==3.8.0
django-background-tasks==1.2.8 django-background-tasks==1.2.8
@ -24,17 +27,20 @@ django-ckeditor==6.7.2
django-cors-headers==4.7.0 django-cors-headers==4.7.0
django-countries==7.6.1 django-countries==7.6.1
django-crispy-forms==2.3 django-crispy-forms==2.3
django-easy-audit==1.3.7
django-extensions==3.2.3 django-extensions==3.2.3
django-filter==25.1 django-filter==25.1
django-import-export==4.3.7 django-import-export==4.3.7
django-js-asset==3.1.2 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-next-url-mixin==0.4.0
django-ordered-model==3.7.4 django-ordered-model==3.7.4
django-phonenumber-field==8.0.0 django-phonenumber-field==8.0.0
django-picklefield==3.3 django-picklefield==3.3
django-plans==2.0.0 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-schema-graph==3.1.0
django-sequences==3.0 django-sequences==3.0
django-tables2==2.7.5 django-tables2==2.7.5
@ -42,29 +48,47 @@ django-treebeard==4.7.1
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
djangorestframework==3.15.2 djangorestframework==3.15.2
djhtml==3.0.7 djhtml==3.0.7
djlint==1.36.4
docopt==0.6.2 docopt==0.6.2
Faker==37.1.0 EditorConfig==0.17.0
Faker==37.3.0
fleming==0.7.0
fonttools==4.57.0 fonttools==4.57.0
fpdf==1.7.2 fpdf==1.7.2
fpdf2==2.8.3 fpdf2==2.8.3
greenlet==3.2.2
h11==0.14.0 h11==0.14.0
httpcore==1.0.7 httpcore==1.0.7
httpx==0.28.1 httpx==0.28.1
icalendar==6.1.2 icalendar==6.1.2
idna==3.10 idna==3.10
jiter==0.9.0 jiter==0.9.0
jsbeautifier==1.15.4
json5==0.12.0
jsonpatch==1.33
jsonpointer==3.0.0
jwt==1.3.1 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 luhnchecker==0.0.12
Markdown==3.7 Markdown==3.8
markdown-it-py==3.0.0 markdown-it-py==3.0.0
mdurl==0.1.2 mdurl==0.1.2
num2words==0.5.14 num2words==0.5.14
numpy==2.2.4 numpy==2.2.4
ofxtools==0.9.5 ofxtools==0.9.5
ollama==0.4.8
openai==1.68.2 openai==1.68.2
opencv-python==4.11.0.86 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 phonenumbers==8.13.42
pillow==10.4.0 pillow==11.2.1
pycparser==2.22 pycparser==2.22
pydantic==2.10.6 pydantic==2.10.6
pydantic_core==2.27.2 pydantic_core==2.27.2
@ -74,24 +98,29 @@ python-slugify==8.0.4
python-stdnum==1.20 python-stdnum==1.20
pytz==2025.2 pytz==2025.2
pyvin==0.0.2 pyvin==0.0.2
PyYAML==6.0.2
pyzbar==0.1.9 pyzbar==0.1.9
redis==3.5.3 redis==3.5.3
regex==2024.11.6
requests==2.32.3 requests==2.32.3
requests-toolbelt==1.0.0
rich==14.0.0 rich==14.0.0
ruff==0.11.10
setuptools==80.3.0 setuptools==80.3.0
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3 sqlparse==0.5.3
suds==1.2.0 suds==1.2.0
swapper==1.3.0 swapper==1.3.0
tablib==3.8.0 tablib==3.8.0
tenacity==9.1.2
text-unidecode==1.3 text-unidecode==1.3
tqdm==4.67.1 tqdm==4.67.1
types-python-dateutil==2.9.0.20241206 types-python-dateutil==2.9.0.20250516
typing_extensions==4.13.0 typing_extensions==4.13.0
tzdata==2025.2 tzdata==2025.2
urllib3==2.3.0 urllib3==2.3.0
wcwidth==0.2.13 wcwidth==0.2.13
langchain zstandard==0.23.0
langchain_ollama
django-easy-audit==1.3.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

View File

@ -243,7 +243,7 @@
<span class="fas fa-ellipsis-h fs-10"></span> <span class="fas fa-ellipsis-h fs-10"></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-end py-2"> <div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'activate_account' 'staff' obj.slug %}"><button class="dropdown-item text-primary">{% trans "Activate" %}</button></a> <a href="{% url 'activate_account' request.dealer.slug 'staff' obj.slug %}"><button class="dropdown-item text-primary">{% trans "Activate" %}</button></a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a href="{% url 'permenant_delete_account' request.dealer.slug 'staff' obj.slug %}"><button class="dropdown-item text-danger">{% trans "Permenantly Delete" %}</button></a> <a href="{% url 'permenant_delete_account' request.dealer.slug 'staff' obj.slug %}"><button class="dropdown-item text-danger">{% trans "Permenantly Delete" %}</button></a>
</div> </div>

View File

@ -60,7 +60,9 @@
{% include "toast-alert.html" %} {% include "toast-alert.html" %}
<main class="main" id="top"> <main class="main" id="top">
{% include 'header.html' %} {% if request.user.is_authenticated %}
{% include 'header.html' %}
{% endif %}
<div class="content"> <div class="content">

View File

@ -3,7 +3,7 @@
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %} {% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
{% block content %} {% block content %}
<div class="row g-3 mt-4"> <div class="row g-3 mt-4 mb-4">
<h2 class="mb-2">{{ _("Leads")|capfirst }}</h2> <h2 class="mb-2">{{ _("Leads")|capfirst }}</h2>
<!-- Action Tracking Modal --> <!-- Action Tracking Modal -->
{% include "crm/leads/partials/update_action.html" %} {% include "crm/leads/partials/update_action.html" %}

View File

@ -4,7 +4,9 @@
<div class="content"> <div class="content">
<h2 class="mb-5">{{ _("Notifications") }}</h2> <h2 class="mb-5">{{ _("Notifications") }}</h2>
<div class="d-flex justify-content-end mb-3">
<a href="{% url 'mark_all_notifications_as_read' %}" class="btn btn-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
</div>
{% if notifications %} {% if notifications %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom"> <div class="mx-n4 mx-lg-n6 mb-5 border-bottom">
{% for notification in notifications %} {% for notification in notifications %}
@ -20,10 +22,7 @@
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 far fa-clock"></span>{{ notification.created }}</p> <p class="text-body-secondary fs-9 mb-0"><span class="me-1 far fa-clock"></span>{{ notification.created }}</p>
</div> </div>
</div> </div>
<div class="dropdown">
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none notification-dropdown-toggle" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2"><a class="dropdown-item" href="{% url 'mark_notification_as_read' notification.id %}">{{ _("Mark as Read")}}</a></div>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -87,7 +87,7 @@
</div> </div>
</div> </div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1"> <div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %} {% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} {% if page_obj.paginator.num_pages > 1 %}

View File

@ -120,10 +120,10 @@
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a class="btn btn-sm btn-phoenix-primary" href="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}"> <a class="btn btn-sm btn-phoenix-primary" href="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}">
{{ _("View Details") }} <i class="fa-solid fa-eye ms-2"></i> <i class="fa-solid fa-eye ms-2"></i>{{ _("View") }}
</a> </a>
<a class="btn btn-sm btn-phoenix-success" href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}"> <a class="btn btn-sm btn-phoenix-success" href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">
{{ _("Update") }} <i class="fa-solid fa-pen ms-2"></i> <i class="fa-solid fa-pen ms-2"></i> {{ _("Update") }}
</a> </a>
</div> </div>
</div> </div>

View File

@ -109,11 +109,7 @@
{{ _("Delete") }} {{ _("Delete") }}
</a> </a>
<a class="btn btn-sm btn-phoenix-secondary" <a class="btn btn-sm btn-phoenix-secondary"
<<<<<<< HEAD
href="{% url 'group_list' request.dealer.slug%}">
=======
href="{% url 'group_list' request.dealer.slug %}"> href="{% url 'group_list' request.dealer.slug %}">
>>>>>>> c9fad7b79c346875a636122fdc7514814180dbc7
<i class="fa-solid fa-arrow-left"></i> <i class="fa-solid fa-arrow-left"></i>
{% trans "Back to List" %} {% trans "Back to List" %}
</a> </a>

View File

@ -32,11 +32,12 @@
<div class="text-danger">{{ error }}</div> <div class="text-danger">{{ error }}</div>
{% endfor %} {% endfor %}
<div class="d-flex mb-3"> <div class="d-flex mb-3">
<a href="{% url 'group_detail' request.dealer.slug group.pk %}" class="btn btn-phoenix-primary me-2 "><i class="fa-solid fa-ban"></i> {% trans "Cancel"|capfirst %}</a> <button class="btn btn-phoenix-primary me-2" type="submit">
<button class="btn btn-phoenix-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i> <i class="fa-solid fa-floppy-disk"></i>
{{ _("Save") }} {{ _("Save") }}
</button> </button>
<a href="{% url 'group_detail' request.dealer.slug group.pk %}" class="btn btn-phoenix-secondary "><i class="fa-solid fa-ban"></i> {% trans "Cancel"|capfirst %}</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -85,7 +85,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'sales_list' request.dealer.slug %}"> <a class="nav-link" href="{% url 'sales_list' request.dealer.slug %}">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@ -93,7 +93,7 @@
</div> </div>
</a> </a>
</li> </li>
{% if perms.django_ledger.view_invoicemodel %} {% if perms.django_ledger.view_invoicemodel %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'invoice_list' request.dealer.slug %}"> <a class="nav-link" href="{% url 'invoice_list' request.dealer.slug %}">
@ -112,7 +112,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -260,7 +260,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.django_ledger.view_purchaseordermodel %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug %}"> <a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug %}">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@ -268,7 +268,7 @@
</div> </div>
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@
<div class="card-header p-2"> <div class="card-header p-2">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h5 class="text-body-emphasis mb-0">{{ _("Notifications") }}</h5> <h5 class="text-body-emphasis mb-0">{{ _("Notifications") }}</h5>
<button class="btn btn-link p-0 fs-9 fw-normal" type="button" id="mark-all-read">{{ _("Mark all as read")}}</button>
</div> </div>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@ -72,7 +72,7 @@
data.notifications.forEach(notification => { data.notifications.forEach(notification => {
seenNotificationIds.add(notification.id); seenNotificationIds.add(notification.id);
if (notification.unread) { if (!notification.is_read) {
unreadCount++; unreadCount++;
} }
}); });
@ -136,7 +136,7 @@
} }
function createNotificationElement(data) { function createNotificationElement(data) {
const isRead = data.read ? 'read' : 'unread'; const isRead = data.is_read ? 'read' : 'unread';
return ` return `
<div class="px-2 px-sm-3 py-3 notification-card position-relative ${isRead} border-bottom" <div class="px-2 px-sm-3 py-3 notification-card position-relative ${isRead} border-bottom"
data-notification-id="${data.id}"> data-notification-id="${data.id}">
@ -223,7 +223,7 @@
if (e.target.classList.contains('mark-as-read')) { if (e.target.classList.contains('mark-as-read')) {
e.preventDefault(); e.preventDefault();
const notificationId = e.target.getAttribute('data-notification-id'); const notificationId = e.target.getAttribute('data-notification-id');
fetch(`/notifications/${notificationId}/mark-read/`, { fetch(`/notifications/${notificationId}/mark_as_read/`, {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRFToken': '{{ csrf_token }}', 'X-CSRFToken': '{{ csrf_token }}',

View File

@ -11,14 +11,13 @@
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.address }}</li> <li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.address }}</li>
</ul> </ul>
<div class="d-flex"> <div class="d-flex">
<a href="{% url 'organization_update' organization.pk %}" class="btn btn-sm btn-phoenix-warning me-2">{% trans "Edit" %}</a> <a href="{% url 'organization_update' request.dealer.slug organization.slug %}" class="btn btn-sm btn-phoenix-primary me-2"><span class="fas fa-edit me-1"></span>{% trans "Edit" %}</a>
<button class="btn btn-phoenix-danger btn-sm delete-btn" <button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'organization_delete' organization.slug %}" data-url="{% url 'organization_delete' request.dealer.slug organization.slug %}"
data-message="Are you sure you want to delete this organization?" data-message="Are you sure you want to delete this organization?"
data-bs-toggle="modal" data-bs-target="#deleteModal"> data-bs-toggle="modal" data-bs-target="#deleteModal">
{% trans 'Delete' %}<i class="fas fa-trash ms-1"></i> <i class="fas fa-trash me-1"></i> {% trans 'Delete' %}
</button> </button>
</div> </div>
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}

View File

@ -11,9 +11,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid mt-4">
<!--Heading--> <!--Heading-->
<h3> <h3 class="mb-3">
{% if object %} {% if object %}
{% trans 'Update Organization'%} {% trans 'Update Organization'%}
{% else %} {% else %}

View File

@ -1,5 +1,5 @@
{% load i18n static %} {% load i18n static %}
<div class="d-flex justify-content-between align-items-center mt-4"> <div class="d-flex justify-content-between align-items-center mt-4 mb-3">
<div class="text-body-secondary"> <div class="text-body-secondary">
{{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }} {{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }}
{{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }} {{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }}

View File

@ -96,7 +96,6 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="account">Account</label> <label for="account">Account</label>
<select class="form-control" name="account" id="account"> <select class="form-control" name="account" id="account">

View File

@ -82,7 +82,7 @@
<div class="card border-light h-100"> <div class="card border-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5> <h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5>
<p class="h5">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p> <p class="h5"><span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount|currency_format }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -101,7 +101,7 @@
<div class="card border-light h-100"> <div class="card border-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5> <h5 class="h6 text-muted mb-2">{% trans 'Purchase Order Amount' %}</h5>
<p class="h5">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p> <p class="h5"><span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount|currency_format }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -120,7 +120,7 @@
<div class="card border-light h-100"> <div class="card border-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5> <h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5>
<p class="h5">{{ po_model.po_amount|currency_format }}{% currency_symbol %}</p> <p class="h5">{{ po_model.po_amount|currency_format }}<span class="currency">{{CURRENCY}}</span></p>
</div> </div>
</div> </div>
</div> </div>
@ -128,7 +128,7 @@
<div class="card border-light h-100"> <div class="card border-light h-100">
<div class="card-body"> <div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'Received Amount' %}</h5> <h5 class="h6 text-muted mb-2">{% trans 'Received Amount' %}</h5>
<p class="h5 text-success">{{ po_model.po_amount_received|currency_format }}{% currency_symbol %}</p> <p class="h5 text-success">{{ po_model.po_amount_received|currency_format }}<span class="currency">{{CURRENCY}}</span></p>
</div> </div>
</div> </div>
</div> </div>
@ -148,7 +148,7 @@
<div class="card-body"> <div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5> <h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<p class="h5 mb-0 me-2">{% currency_symbol %}{{ po_model.po_amount|currency_format }}</p> <p class="h5 mb-0 me-2"><span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount|currency_format }}</p>
<span class="badge bg-success"> <span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>{% trans 'Fulfilled' %} <i class="fas fa-check-circle me-1"></i>{% trans 'Fulfilled' %}
</span> </span>

View File

@ -3,19 +3,19 @@
{% block content %} {% block content %}
<form action="{% url 'inventory_item_create' request.dealer.slug po_model.pk %}" method="post"> <form action="{% url 'inventory_item_create' request.dealer.slug po_model.pk %}" method="post">
{% csrf_token %} {% csrf_token %}
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %}
<div class="form-group"> <div class="form-group">
<label for="account">Account</label> <label for="account">Account</label>
<select class="form-control" name="account" id="account"> <select class="form-control" name="account" id="account">
{% for account in inventory_accounts %} {% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option> <option value="{{ account.pk }}">{{ account }}"></option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="quantity">Quantity</label> <label for="quantity">Quantity</label>

View File

@ -18,18 +18,20 @@
<table class="table table-hover table-bordered"> <table class="table table-hover table-bordered">
<thead class=""> <thead class="">
<tr> <tr>
<th class="d-flex justify-content-between align-items-center"> <th style="min-width: 600px;" class="d-flex justify-content-between align-items-center">
{% trans 'Item' %} {% trans 'Item' %}
{% if po_model.is_draft %}
<button type="button" <button type="button"
class="btn btn-sm btn-phoenix-success" class="btn btn-sm btn-phoenix-success"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#mainModal" data-bs-target="#mainModal"
hx-get="{% url 'inventory_item_create' dealer_slug %}?for_po=1" hx-get="{% url 'inventory_item_create' dealer_slug %}?for_po=1"
hx-target=".main-modal-body" hx-target=".main-modal-body"
hx-select="form" hx-select="form"
hx-swap="innerHTML"> hx-swap="innerHTML">
<i class="fas fa-plus me-1"></i>{% trans 'Add Item' %} <i class="fas fa-plus me-1"></i>{% trans 'Add Item' %}
</button> </button>
{% endif %}
</th> </th>
<th>{% trans 'Unit Cost' %}</th> <th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'Quantity' %}</th> <th>{% trans 'Quantity' %}</th>
@ -60,7 +62,7 @@
<td id="{{ f.instance.html_id_quantity }}">{{ f.po_quantity|add_class:"form-control" }}</td> <td id="{{ f.instance.html_id_quantity }}">{{ f.po_quantity|add_class:"form-control" }}</td>
<td>{{ f.entity_unit|add_class:"form-control" }}</td> <td>{{ f.entity_unit|add_class:"form-control" }}</td>
<td class="text-end" id="{{ f.instance.html_id_total_amount }}"> <td class="text-end" id="{{ f.instance.html_id_total_amount }}">
{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</td> <span class="currency">{{CURRENCY}}</span>{{ f.instance.po_total_amount | currency_format }}</td>
<td>{{ f.po_item_status|add_class:"form-control" }}</td> <td>{{ f.po_item_status|add_class:"form-control" }}</td>
{% if itemtxs_formset.can_delete %} {% if itemtxs_formset.can_delete %}
<td class="text-center"> <td class="text-center">
@ -96,7 +98,7 @@
<th></th> <th></th>
<th></th> <th></th>
<th class="text-end">{% trans 'Total' %}</th> <th class="text-end">{% trans 'Total' %}</th>
<th class="text-end">{% currency_symbol %}{{ po_model.po_amount | currency_format }}</th> <th class="text-end"><span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | currency_format }}</th>
<th></th> <th></th>
{% if itemtxs_formset.can_delete %} {% if itemtxs_formset.can_delete %}
<th></th> <th></th>

View File

@ -21,7 +21,7 @@
<td>{{ po.po_title }}</td> <td>{{ po.po_title }}</td>
<td>{{ po.get_status_action_date }}</td> <td>{{ po.get_status_action_date }}</td>
<td>{{ po.get_po_status_display }}</td> <td>{{ po.get_po_status_display }}</td>
<td>{% currency_symbol %}{{ po.po_amount | currency_format }}</td> <td><span class="currency">{{CURRENCY}}</span>{{ po.po_amount | currency_format }}</td>
<td class="has-text-centered"> <td class="has-text-centered">
<div class="dropdown is-right is-hoverable" id="bill-action-{{ po.uuid }}"> <div class="dropdown is-right is-hoverable" id="bill-action-{{ po.uuid }}">
<div class="dropdown-trigger"> <div class="dropdown-trigger">

View File

@ -1,8 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load trans from i18n %} {% load trans from i18n %}
{% load static %} {% load static %}
{% load custom_filters %}
{% load django_ledger %} {% load django_ledger %}
{% load custom_filters %}
{% block content %} {% block content %}
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-12">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<div class="row text-center"> <div class="row text-center">
@ -31,7 +31,7 @@
<div class="p-2"> <div class="p-2">
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6> <h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
<h3 class="fw-light mb-0"> <h3 class="fw-light mb-0">
{% currency_symbol %}{{ po_model.po_amount | absolute | currency_format }} <span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | absolute | currency_format }}
</h3> </h3>
</div> </div>
</div> </div>
@ -39,24 +39,24 @@
<div class="p-2"> <div class="p-2">
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6> <h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
<h3 class="fw-light mb-0 text-success"> <h3 class="fw-light mb-0 text-success">
{% currency_symbol %}{{ po_model.po_amount_received | currency_format }} <span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount_received | currency_format }}
</h3> </h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-12">
<div class="table-responsive"> <div class="table-responsive">
{% po_item_table1 po_items %} {% po_item_table1 po_items %}
</div> </div>
</div> </div>
</div>
</div>
</div> </div>
{% include "purchase_orders/includes/mark_as.html" %} {% include "purchase_orders/includes/mark_as.html" %}

View File

@ -65,9 +65,9 @@
<div class="btn-reveal-trigger position-static"> <div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button> <button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2"> <div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'purchase_order_detail' request.dealer.slug po.pk %}" class="dropdown-item text-success-dark">{% trans 'Detail' %}</a> <a href="{% url 'purchase_order_detail' request.dealer.slug po.pk %}" class="dropdown-item text-success-dark">{% trans 'Purchase Order Detail' %}</a>
{% if po.po_status == 'fulfilled' %} {% if po.po_status == 'fulfilled' %}
<a href="{% url 'view_items_inventory' dealer_slug=request.dealer.slug entity_slug=entity_slug po_pk=po.pk %}" class="dropdown-item text-success-dark">{% trans 'View Inventory Items' %}</a> <a href="{% url 'view_items_inventory' dealer_slug=request.dealer.slug entity_slug=entity_slug po_pk=po.pk %}" class="dropdown-item text-success-dark">{% trans 'Add Inventory Items' %}</a>
{% else %} {% else %}
<button class="dropdown-item text-warning-dark" disabled><span class="fas fa-exclamation-triangle me-1"></span> Fulfill the PO Before Viewing Inventory</button> <button class="dropdown-item text-warning-dark" disabled><span class="fas fa-exclamation-triangle me-1"></span> Fulfill the PO Before Viewing Inventory</button>
{% endif %} {% endif %}

View File

@ -61,7 +61,7 @@
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th>{% currency_symbol %}{{ ce_cost_estimate__sum | currency_format }}</th> <th><span class="currency">{{CURRENCY}}</span>{{ ce_cost_estimate__sum | currency_format }}</th>
</tr> </tr>
</tfoot> </tfoot>
<tbody> <tbody>
@ -69,8 +69,8 @@
<tr> <tr>
<td>{{ i.item_model__name }}</td> <td>{{ i.item_model__name }}</td>
<td>{{ i.ce_quantity__sum }}</td> <td>{{ i.ce_quantity__sum }}</td>
<td>{% currency_symbol %}{{ i.avg_unit_cost | currency_format }}</td> <td><span class="currency">{{CURRENCY}}</span>{{ i.avg_unit_cost | currency_format }}</td>
<td>{% currency_symbol %}{{ i.ce_cost_estimate__sum | currency_format }}</td> <td><span class="currency">{{CURRENCY}}</span>{{ i.ce_cost_estimate__sum | currency_format }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -20,7 +20,7 @@
<td class="has-text-centered">{{ item.po_unit_cost }}</td> <td class="has-text-centered">{{ item.po_unit_cost }}</td>
<td class="has-text-centered">{{ item.po_quantity }}</td> <td class="has-text-centered">{{ item.po_quantity }}</td>
<td class="{% if item.is_cancelled %}djl-is-strikethrough{% endif %} has-text-centered"> <td class="{% if item.is_cancelled %}djl-is-strikethrough{% endif %} has-text-centered">
{% currency_symbol %}{{ item.po_total_amount | currency_format }}</td> <span class="currency">{{CURRENCY}}</span>{{ item.po_total_amount | currency_format }}</td>
<td class="has-text-weight-bold has-text-centered {% if item.is_cancelled %}has-text-danger{% endif %}"> <td class="has-text-weight-bold has-text-centered {% if item.is_cancelled %}has-text-danger{% endif %}">
{% if item.po_item_status %} {% if item.po_item_status %}
{{ item.get_po_item_status_display }} {{ item.get_po_item_status_display }}
@ -40,7 +40,7 @@
<td></td> <td></td>
<td class="has-text-right">{% trans 'Total PO Amount' %}</td> <td class="has-text-right">{% trans 'Total PO Amount' %}</td>
<td class="has-text-weight-bold has-text-centered"> <td class="has-text-weight-bold has-text-centered">
{% currency_symbol %}{{ po_model.po_amount | currency_format }}</td> <span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | currency_format }}</td>
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>

View File

@ -72,7 +72,7 @@
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center mb-4">
<div class="col-lg-10"> <div class="col-lg-10">
<div class="card shadow"> <div class="card shadow">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">

View File

@ -42,7 +42,6 @@
<td class="align-middle white-space-nowrap ps-1"> <td class="align-middle white-space-nowrap ps-1">
<div> <div>
<a class="fs-8 fw-bold" href="{% url 'user_detail' request.dealer.slug user.slug%}">{{ user.arabic_name }}</a> <a class="fs-8 fw-bold" href="{% url 'user_detail' request.dealer.slug user.slug%}">{{ user.arabic_name }}</a>
{{user.dealer}}
</div> </div>
</td> </td>
<td class="align-middle white-space-nowrap align-items-center">{{ user.email }}</td> <td class="align-middle white-space-nowrap align-items-center">{{ user.email }}</td>