Compare commits
15 Commits
2b3834ac10
...
5fe6ee8d17
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fe6ee8d17 | |||
| debe1fec9d | |||
| e4a78e1bbd | |||
| b49c35adc2 | |||
| 525a62ab3a | |||
| 2bb0dea9fa | |||
| 8f14f9bfc9 | |||
| 93b829fd51 | |||
| 73ae641ddb | |||
| 286031b4d8 | |||
| 28f62ae55a | |||
| 473d7e1990 | |||
| 1e7ed1f88d | |||
| c4a57f4803 | |||
| eb326597a5 |
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
""",
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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 |
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }}',
|
||||
|
||||
@ -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' %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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") }}
|
||||
|
||||
@ -96,7 +96,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="account">Account</label>
|
||||
<select class="form-control" name="account" id="account">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user