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.contrib.auth import get_user_model
from inventory.tasks import create_coa_accounts
from inventory.models import Dealer
User = get_user_model()
from inventory.tasks import long_running_task
from django_q.tasks import async_task
class Command(BaseCommand):
def handle(self, *args, **kwargs):
# user = User.objects.last()
# print(user.email)
# # 2. Force email confirmation
# # email = user.emailaddress_set.first()
# confirmation = EmailConfirmation.create(user.email)
# confirmation.send()
# result = re.match(r'^05\d{8}$', '0625252522')
# print(result)
dealer = Dealer.objects.last()
create_coa_accounts(dealer.pk)
async_task(long_running_task, 20)

View File

@ -10,38 +10,38 @@ from inventory.utils import get_user_type
logger = logging.getLogger("user_activity")
class LogUserActivityMiddleware:
"""
Middleware for logging user activity.
# class LogUserActivityMiddleware:
# """
# Middleware for logging user activity.
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
ID, the action performed, and the timestamp. It is intended to assist in
tracking user actions across the application for analytics or auditing purposes.
# 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
# ID, the action performed, and the timestamp. It is intended to assist in
# tracking user actions across the application for analytics or auditing purposes.
:ivar get_response: The next middleware or view in the WSGI request-response
chain.
:type get_response: Callable
"""
# :ivar get_response: The next middleware or view in the WSGI request-response
# chain.
# :type get_response: Callable
# """
def __init__(self, get_response):
self.get_response = get_response
# def __init__(self, get_response):
# self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# def __call__(self, request):
# response = self.get_response(request)
if request.user.is_authenticated:
action = f"{request.method} {request.path}"
models.UserActivityLog.objects.create(
user=request.user, action=action, timestamp=timezone.now()
)
return response
# if request.user.is_authenticated:
# action = f"{request.method} {request.path}"
# models.UserActivityLog.objects.create(
# user=request.user, action=action, timestamp=timezone.now()
# )
# return response
def get_client_ip(self, request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
return x_forwarded_for.split(",")[0]
return request.META.get("REMOTE_ADDR")
# def get_client_ip(self, request):
# x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
# if x_forwarded_for:
# return x_forwarded_for.split(",")[0]
# return request.META.get("REMOTE_ADDR")
class InjectParamsMiddleware:
@ -93,12 +93,13 @@ class InjectDealerMiddleware:
def __call__(self, request):
try:
request.is_dealer = False
request.is_staff = False
if hasattr(request.user, "dealer"):
request.is_dealer = True
elif hasattr(request.user, "staffmember"):
request.is_staff = True
if request.user.is_authenticated:
request.is_dealer = False
request.is_staff = False
if hasattr(request.user, "dealer"):
request.is_dealer = True
elif hasattr(request.user, "staffmember"):
request.is_staff = True
except Exception:
pass
response = self.get_response(request)
@ -120,14 +121,30 @@ class DealerSlugMiddleware:
def __call__(self, request):
response = self.get_response(request)
return response
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:
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
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
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")
return None

View File

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

View File

@ -1,4 +1,6 @@
from decimal import Decimal
from django.urls import reverse
from inventory.tasks import create_coa_accounts, create_make_accounts
from django.contrib.auth.models import Group
from django.db.models.signals import post_save, post_delete
@ -18,7 +20,7 @@ from django_ledger.models import (
from . import models
from django.utils.timezone import now
from django.db import transaction
from django_q.tasks import async_task
User = get_user_model()
@ -143,7 +145,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task
create_coa_accounts(instance.pk)
async_task(create_coa_accounts,instance.pk)
# create_settings(instance.pk)
# create_accounts_for_make(instance.pk)
@ -164,12 +166,12 @@ def create_dealer_groups(sender, instance, created, **kwargs):
:param kwargs: Additional keyword arguments passed by the signal.
:type kwargs: dict
"""
group_names = ["Inventory", "Accountant", "Sales"]
group_names = ["Inventory", "Accountant", "Sales","Manager"]
def create_groups():
for group_name in group_names:
group, created = Group.objects.get_or_create(
name=f"{instance.pk}_{group_name}"
name=f"{instance.slug}_{group_name}"
)
group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=instance, group=group
@ -896,7 +898,86 @@ def update_finance_cost(sender, instance, created, **kwargs):
@receiver(post_save, sender=PurchaseOrderModel)
def create_po_item_upload(sender,instance,created,**kwargs):
if instance.po_status == "fulfilled":
for item in instance.get_itemtxs_data()[0]:
if instance.po_status == "fulfilled":
for item in instance.get_itemtxs_data()[0]:
dealer = models.Dealer.objects.get(entity=instance.entity)
models.PoItemsUploaded.objects.create(dealer=dealer,po=instance, item=item, status="fulfilled")
##########################################################
######################Notification########################
##########################################################
@receiver(post_save, sender=models.Car)
def car_created_notification(sender, instance, created, **kwargs):
if created:
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email)
for accountant in accountants:
models.Notification.objects.create(
user=accountant,
message=f"""
New Car {instance.vin} has been added to dealer {instance.dealer.name}.
<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_ledger.io import roles
from django.core.mail import send_mail
from background_task import background
from django.utils.translation import gettext_lazy as _
from inventory.models import DealerSettings, Dealer
from django_q.tasks import async_task
# @background
def create_settings(pk):
instance = Dealer.objects.get(pk=pk)
@ -34,7 +32,6 @@ def create_settings(pk):
)
@background
def create_coa_accounts(pk):
with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk)
@ -772,8 +769,6 @@ def create_coa_accounts(pk):
except Exception as e:
print(e)
@background
def create_coa_accounts1(pk):
with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk)
@ -1456,25 +1451,16 @@ def create_make_accounts(entity, coa, makes, name, role, balance_type):
)
return acc
@background
def send_email(from_, to_, subject, message):
subject = subject
message = message
from_email = from_
recipient_list = [to_]
send_mail(subject, message, from_email, recipient_list)
async_task(send_mail,subject, message, from_email, recipient_list)
@background
def long_running_task(task_id, *args, **kwargs):
# @background
def long_running_task(duration):
"""Example background task"""
print(f"Starting task {task_id} with args: {args}, kwargs: {kwargs}")
# Simulate work
for i in range(5):
print(f"Task {task_id} progress: {i + 1}/5")
result = f"Task {task_id} completed at {datetime.now()}"
print(result)
return result
print("Task completed")
return True

View File

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

View File

@ -8,44 +8,12 @@ from django.conf.urls import handler403, handler400, handler404, handler500
urlpatterns = [
# main URLs
path("", views.WelcomeView, name="welcome"),
path("signup/", views.dealer_signup, name="account_signup"),
path("", 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
path("tasks/", views.task_list, name="task_list"),
path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards
@ -234,38 +202,29 @@ urlpatterns = [
),
# 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(
"crm/notifications/",
"notifications/",
views.NotificationListView.as_view(),
name="notifications_history",
),
path(
"crm/fetch_notifications/",
views.fetch_notifications,
name="fetch_notifications",
),
path(
"crm/notifications/<int:notification_id>/mark_as_read/",
"notifications/<int:notification_id>/mark_as_read/",
views.mark_notification_as_read,
name="mark_notification_as_read",
),
path(
"notifications/mark_all_notifications_as_read/",
views.mark_all_notifications_as_read,
name="mark_all_notifications_as_read",
),
# #######################
# #######################
path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"),
#######################################################
# Vendor URLs

View File

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

View File

@ -8,7 +8,6 @@ import logging
import tempfile
import numpy as np
from time import sleep
# from rich import print
from random import randint
from decimal import Decimal
@ -23,7 +22,6 @@ from urllib.parse import urlparse, urlunparse
from inventory.mixins import DealerSlugMixin
from inventory.models import Status as LeadStatus
from django.db import IntegrityError
from background_task.models import Task
from django.views.generic import FormView
from django.views.decorators.http import require_http_methods
from django.db.models.deletion import RestrictedError
@ -268,8 +266,10 @@ def switch_language(request):
logger.warning(f"Invalid language code: {language}")
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
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")
class WelcomeView(TemplateView):
def WelcomeView(request):
"""
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.
:type template_name: str
"""
template_name = "welcome.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
plan_list = Plan.objects.all()
context["plan_list"] = plan_list
return context
if request.user.is_authenticated:
return redirect("home", dealer_slug=request.dealer.slug)
plan_list = Plan.objects.all()
context = {"plan_list": plan_list}
return render(request, "welcome.html", context)
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -1091,7 +1088,7 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"sold": cars.filter(status="sold").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["year"] = models.Car.objects.none()
make = self.request.GET.get("make")
@ -1573,7 +1570,7 @@ class CarDeleteView(
model = models.Car
template_name = "inventory/car_confirm_delete.html"
permission_required = ["inventory.delete_car"]
def delete(self, request, *args, **kwargs):
@ -1584,14 +1581,14 @@ class CarDeleteView(
Returns the URL to redirect to after a successful car deletion.
It dynamically includes the dealer_slug from the URL.
"""
dealer_slug = self.kwargs.get('dealer_slug')
if dealer_slug:
return reverse_lazy("car_list", kwargs={'dealer_slug': dealer_slug})
else:
messages.error(self.request, _("Could not determine dealer for redirection."))
return reverse_lazy("home")
return reverse_lazy("home")
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -2068,7 +2065,7 @@ class DealerDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
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
cars_count = models.Car.objects.filter(dealer=dealer).count()
@ -2497,7 +2494,7 @@ class VendorCreateView(
)
else:
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)
form.instance.dealer = dealer
form.instance.save()
@ -2671,9 +2668,17 @@ class GroupCreateView(
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
group = Group.objects.create(name=f"{dealer.slug}_{instance.name}")
instance.dealer = dealer
instance.group = group
group_name = f"{dealer.slug}_{instance.name}"
group,created = Group.objects.get_or_create(name=group_name)
if created:
group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=dealer, group=group
)
group_manager.set_default_permissions()
dealer.user.groups.add(group)
else:
instance.dealer = dealer
instance.group = group
instance.save()
return super().form_valid(form)
@ -2952,10 +2957,10 @@ class UserCreateView(
staff.staff_member = staff_member
staff.dealer = dealer
staff.add_as_manager()
group = Group.objects.filter(customgroup__name__iexact=staff.staff_type).first()
group = models.CustomGroup.objects.filter(dealer=dealer,name__iexact=staff.staff_type).first()
staff.save()
if group:
staff.add_group(group)
staff.add_group(group.group)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("user_list", args=[self.request.dealer.slug])
@ -5610,6 +5615,10 @@ def lead_transfer(request,dealer_slug, slug):
if form.is_valid():
lead.staff = form.cleaned_data["transfer_to"]
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"))
else:
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})
# Background Tasks
def task_list(request):
# Get all tasks ordered by creation time
tasks = Task.objects.all()
# Add pagination
paginator = Paginator(tasks, 10) # Show 10 tasks per page
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "tasks/task_list.html", {"page_obj": page_obj})
def sse_stream(request):
print("hi")
def event_stream():
last_id = request.GET.get("last_id", 0)
while True:
@ -8834,7 +8831,7 @@ def mark_notification_as_read(request, notification_id):
notification = get_object_or_404(
models.Notification, id=notification_id, user=request.user
)
notification.read = True
notification.is_read = True
notification.save()
return JsonResponse({"status": "success"})
@ -8842,9 +8839,9 @@ def mark_notification_as_read(request, notification_id):
@login_required
def mark_all_notifications_as_read(request):
models.Notification.objects.filter(user=request.user, is_read=False).update(
read=True
is_read=True
)
return JsonResponse({"status": "success"})
return redirect(request.META.get("HTTP_REFERER"))
@login_required
@ -9251,7 +9248,7 @@ def InventoryItemCreateView(request, dealer_slug):
form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
context = {
"make_data": models.CarMake.objects.all(),
"make_data": models.CarMake.objects.filter(is_sa_import=True),
"inventory_accounts": inventory_accounts,
"cogs_accounts": cogs_accounts,
"form": form,
@ -9261,7 +9258,7 @@ def InventoryItemCreateView(request, dealer_slug):
request,
"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,
"cogs_accounts": cogs_accounts,
},
@ -9325,6 +9322,74 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
dealer = get_user_type(self.request)
entity = dealer.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):
dealer = get_user_type(self.request)
@ -9535,7 +9600,7 @@ class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_fulfilled"
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_canceled"
@ -9670,7 +9735,7 @@ def upload_cars(request, dealer_slug, pk=None):
if not csv_file.name.endswith(".csv"):
messages.error(request, "Please upload a CSV file")
return redirect("upload_cars", dealer_slug=dealer_slug)
return response
try:
# Read the file content
file_content = csv_file.read().decode("utf-8")
@ -9716,17 +9781,23 @@ def upload_cars(request, dealer_slug, pk=None):
po_item.save()
messages.success(request, f"Successfully imported {cars_created} cars")
return response
return redirect(
"view_items_inventory",
dealer_slug=dealer_slug,
slug_entity=dealer.entity.slug,
po_pk=item.po_model.pk,)
except Exception as e:
messages.error(request, f"Error processing CSV: {str(e)}")
return response
form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.all()
return render(
request,
"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
attrs==25.3.0
Babel==2.15.0
beautifulsoup4==4.13.4
blessed==1.21.0
cattrs==24.1.3
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.2.1
colorama==0.4.6
crispy-bootstrap5==2024.10
cryptography==44.0.2
cssbeautifier==1.15.4
defusedxml==0.7.1
diff-match-patch==20241021
distro==1.9.0
Django==5.1.7
Django==5.2.3
django-allauth==65.6.0
django-appointment==3.8.0
django-background-tasks==1.2.8
@ -24,17 +27,20 @@ django-ckeditor==6.7.2
django-cors-headers==4.7.0
django-countries==7.6.1
django-crispy-forms==2.3
django-easy-audit==1.3.7
django-extensions==3.2.3
django-filter==25.1
django-import-export==4.3.7
django-js-asset==3.1.2
django-ledger==0.7.6.1
django-ledger==0.7.7
django-manager-utils==3.1.5
django-next-url-mixin==0.4.0
django-ordered-model==3.7.4
django-phonenumber-field==8.0.0
django-picklefield==3.3
django-plans==2.0.0
django-q==1.3.9
django-q2==1.8.0
django-query-builder==3.2.0
django-schema-graph==3.1.0
django-sequences==3.0
django-tables2==2.7.5
@ -42,29 +48,47 @@ django-treebeard==4.7.1
django-widget-tweaks==1.5.0
djangorestframework==3.15.2
djhtml==3.0.7
djlint==1.36.4
docopt==0.6.2
Faker==37.1.0
EditorConfig==0.17.0
Faker==37.3.0
fleming==0.7.0
fonttools==4.57.0
fpdf==1.7.2
fpdf2==2.8.3
greenlet==3.2.2
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
icalendar==6.1.2
idna==3.10
jiter==0.9.0
jsbeautifier==1.15.4
json5==0.12.0
jsonpatch==1.33
jsonpointer==3.0.0
jwt==1.3.1
langchain==0.3.25
langchain-core==0.3.61
langchain-ollama==0.3.3
langchain-text-splitters==0.3.8
langsmith==0.3.42
luhnchecker==0.0.12
Markdown==3.7
Markdown==3.8
markdown-it-py==3.0.0
mdurl==0.1.2
num2words==0.5.14
numpy==2.2.4
ofxtools==0.9.5
ollama==0.4.8
openai==1.68.2
opencv-python==4.11.0.86
orjson==3.10.18
packaging==24.2
pandas==2.2.3
pathspec==0.12.1
phonenumbers==8.13.42
pillow==10.4.0
pillow==11.2.1
pycparser==2.22
pydantic==2.10.6
pydantic_core==2.27.2
@ -74,24 +98,29 @@ python-slugify==8.0.4
python-stdnum==1.20
pytz==2025.2
pyvin==0.0.2
PyYAML==6.0.2
pyzbar==0.1.9
redis==3.5.3
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
rich==14.0.0
ruff==0.11.10
setuptools==80.3.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3
suds==1.2.0
swapper==1.3.0
tablib==3.8.0
tenacity==9.1.2
text-unidecode==1.3
tqdm==4.67.1
types-python-dateutil==2.9.0.20241206
types-python-dateutil==2.9.0.20250516
typing_extensions==4.13.0
tzdata==2025.2
urllib3==2.3.0
wcwidth==0.2.13
langchain
langchain_ollama
django-easy-audit==1.3.7
zstandard==0.23.0

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>
</button>
<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>
<a href="{% url 'permenant_delete_account' request.dealer.slug 'staff' obj.slug %}"><button class="dropdown-item text-danger">{% trans "Permenantly Delete" %}</button></a>
</div>

View File

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

View File

@ -3,7 +3,7 @@
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
{% 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>
<!-- Action Tracking Modal -->
{% include "crm/leads/partials/update_action.html" %}

View File

@ -4,7 +4,9 @@
<div class="content">
<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 %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">
{% 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>
</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>
{% endfor %}
</div>

View File

@ -87,7 +87,7 @@
</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' %}
</div>
{% if page_obj.paginator.num_pages > 1 %}

View File

@ -120,10 +120,10 @@
<div class="d-flex gap-2">
<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 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>
</div>
</div>

View File

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

View File

@ -32,11 +32,12 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
<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" type="submit">
<button class="btn btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
{{ _("Save") }}
</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>
</form>
</div>

View File

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

View File

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

View File

@ -11,14 +11,13 @@
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.address }}</li>
</ul>
<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"
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-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>
</div>
</div>
{% include 'modal/delete_modal.html' %}

View File

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

View File

@ -1,5 +1,5 @@
{% 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">
{{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }}
{{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }}

View File

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

View File

@ -82,7 +82,7 @@
<div class="card border-light h-100">
<div class="card-body">
<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>
@ -101,7 +101,7 @@
<div class="card border-light h-100">
<div class="card-body">
<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>
@ -120,7 +120,7 @@
<div class="card border-light h-100">
<div class="card-body">
<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>
@ -128,7 +128,7 @@
<div class="card border-light h-100">
<div class="card-body">
<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>
@ -148,7 +148,7 @@
<div class="card-body">
<h5 class="h6 text-muted mb-2">{% trans 'PO Amount' %}</h5>
<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">
<i class="fas fa-check-circle me-1"></i>{% trans 'Fulfilled' %}
</span>

View File

@ -3,19 +3,19 @@
{% 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 %}
{% 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="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 %}
<div class="form-group">
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
{% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option>
{% endfor %}
</select>
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
{% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="quantity">Quantity</label>

View File

@ -18,18 +18,20 @@
<table class="table table-hover table-bordered">
<thead class="">
<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' %}
{% if po_model.is_draft %}
<button type="button"
class="btn btn-sm btn-phoenix-success"
data-bs-toggle="modal"
data-bs-target="#mainModal"
hx-get="{% url 'inventory_item_create' dealer_slug %}?for_po=1"
hx-target=".main-modal-body"
hx-select="form"
hx-swap="innerHTML">
<i class="fas fa-plus me-1"></i>{% trans 'Add Item' %}
</button>
class="btn btn-sm btn-phoenix-success"
data-bs-toggle="modal"
data-bs-target="#mainModal"
hx-get="{% url 'inventory_item_create' dealer_slug %}?for_po=1"
hx-target=".main-modal-body"
hx-select="form"
hx-swap="innerHTML">
<i class="fas fa-plus me-1"></i>{% trans 'Add Item' %}
</button>
{% endif %}
</th>
<th>{% trans 'Unit Cost' %}</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>{{ f.entity_unit|add_class:"form-control" }}</td>
<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>
{% if itemtxs_formset.can_delete %}
<td class="text-center">
@ -96,7 +98,7 @@
<th></th>
<th></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>
{% if itemtxs_formset.can_delete %}
<th></th>

View File

@ -21,7 +21,7 @@
<td>{{ po.po_title }}</td>
<td>{{ po.get_status_action_date }}</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">
<div class="dropdown is-right is-hoverable" id="bill-action-{{ po.uuid }}">
<div class="dropdown-trigger">

View File

@ -1,8 +1,8 @@
{% extends 'base.html' %}
{% load trans from i18n %}
{% load static %}
{% load custom_filters %}
{% load django_ledger %}
{% load custom_filters %}
{% block content %}
<div class="container-fluid mt-4">
@ -23,7 +23,7 @@
</div>
</div>
<div class="col-lg-12">
<div class="col-lg-12">
<div class="card mb-4">
<div class="card-body">
<div class="row text-center">
@ -31,7 +31,7 @@
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
<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>
</div>
</div>
@ -39,24 +39,24 @@
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-12">
<div class="col-lg-12">
<div class="table-responsive">
{% po_item_table1 po_items %}
</div>
</div>
</div>
</div>
</div>
{% include "purchase_orders/includes/mark_as.html" %}

View File

@ -65,9 +65,9 @@
<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>
<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' %}
<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 %}
<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 %}

View File

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

View File

@ -20,7 +20,7 @@
<td class="has-text-centered">{{ item.po_unit_cost }}</td>
<td class="has-text-centered">{{ item.po_quantity }}</td>
<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 %}">
{% if item.po_item_status %}
{{ item.get_po_item_status_display }}
@ -40,7 +40,7 @@
<td></td>
<td class="has-text-right">{% trans 'Total PO Amount' %}</td>
<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>
</tr>

View File

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

View File

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