fix the notificatin and notification counter

This commit is contained in:
ismail 2025-06-29 17:02:32 +03:00
parent 473d7e1990
commit 73ae641ddb
20 changed files with 155 additions and 138 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

@ -126,7 +126,9 @@ class DealerSlugMiddleware:
request.path_info.startswith('/en/login/') or \ request.path_info.startswith('/en/login/') or \
request.path_info.startswith('/en/logout/') 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('/ar/ledger/'): request.path_info.startswith('/en/ledger/') or \
request.path_info.startswith('/en/notifications/') or \
request.path_info.startswith('/ar/notifications/'):
return None return None
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -141,6 +143,7 @@ class DealerSlugMiddleware:
return None return None
if dealer_slug.lower() != request.dealer.slug.lower(): if dealer_slug.lower() != request.dealer.slug.lower():
print(dealer_slug)
logger.warning(f"Dealer slug mismatch: {dealer_slug} != {request.dealer.slug}") logger.warning(f"Dealer slug mismatch: {dealer_slug} != {request.dealer.slug}")
raise Http404("Dealer slug mismatch") raise Http404("Dealer slug mismatch")

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

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,26 @@ 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>
""",
)

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

@ -14,7 +14,6 @@ urlpatterns = [
path("<slug:dealer_slug>/", views.HomeView.as_view(), name="home"), path("<slug:dealer_slug>/", views.HomeView.as_view(), name="home"),
# Tasks # Tasks
path("<slug:dealer_slug>/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
@ -203,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

@ -23,7 +23,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
@ -2670,9 +2669,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)
@ -2951,10 +2958,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])
@ -8777,20 +8784,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:
@ -8833,7 +8828,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"})
@ -8841,9 +8836,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
@ -9669,7 +9664,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")
@ -9715,10 +9710,16 @@ 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()

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

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

@ -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

@ -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

@ -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

@ -18,7 +18,7 @@
<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 %} {% if po_model.is_draft %}
<button type="button" <button type="button"
@ -62,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">
@ -98,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

@ -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

@ -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>