Compare commits

...

10 Commits

13 changed files with 485 additions and 228 deletions

View File

@ -135,10 +135,15 @@ class StaffForm(forms.ModelForm):
),
label=_("Phone Number"),
)
group = forms.ModelMultipleChoiceField(
label=_("Group"),
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
queryset=CustomGroup.objects.all(),
required=True,
)
class Meta:
model = Staff
fields = ["name", "arabic_name", "phone_number", "staff_type"]
fields = ["name", "arabic_name", "phone_number", "group"]
# Dealer Form
@ -1523,44 +1528,116 @@ class GroupForm(forms.ModelForm):
fields = ["name"]
# class PermissionForm(forms.ModelForm):
# """
# Represents a form for managing permissions using a multiple-choice field.
# This class is a Django ModelForm that is used to handle permission
# assignments. It provides a multiple selection widget pre-populated with
# permissions based on specific app labels. The form offers a way to submit
# and validate permission data for further processing.
# :ivar name: A multiple-choice field that allows users to select permissions
# related to specific app labels (`inventory` and `django_ledger`).
# :type name: ModelMultipleChoiceField
# """
# name = forms.ModelMultipleChoiceField(
# queryset=cache.get(
# "permissions_queryset",
# Permission.objects.filter(
# content_type__app_label__in=["inventory", "django_ledger"]
# ),
# ),
# widget=forms.CheckboxSelectMultiple(),
# required=True,
# )
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# cache.set(
# "permissions_queryset",
# Permission.objects.filter(
# content_type__app_label__in=["inventory", "django_ledger"]
# ),
# 60 * 60,
# )
# class Meta:
# model = Permission
# fields = ["name"]
class PermissionForm(forms.ModelForm):
"""
Represents a form for managing permissions using a multiple-choice field.
This class is a Django ModelForm that is used to handle permission
assignments. It provides a multiple selection widget pre-populated with
permissions based on specific app labels. The form offers a way to submit
and validate permission data for further processing.
:ivar name: A multiple-choice field that allows users to select permissions
related to specific app labels (`inventory` and `django_ledger`).
:type name: ModelMultipleChoiceField
Form for managing permissions with grouped checkboxes by app and model.
"""
name = forms.ModelMultipleChoiceField(
queryset=cache.get(
"permissions_queryset",
Permission.objects.filter(
content_type__app_label__in=["inventory", "django_ledger"]
),
),
widget=forms.CheckboxSelectMultiple(),
required=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
cache.set(
EXCLUDED_MODELS = [
"inventory.car",
"inventory.carfinance",
"inventory.carlocation",
"inventory.customcard",
"inventory.cartransfer",
"inventory.carcolors",
"inventory.carequipment",
"inventory.interiorcolors",
"inventory.exteriorcolors",
"inventory.lead",
"inventory.customgroup",
"inventory.saleorder",
"inventory.payment",
"inventory.staff",
"inventory.schedule",
"inventory.activity",
"inventory.opportunity",
"inventory.carreservation"
"inventory.customer",
"inventory.organization",
# "inventory.salequotation",
# "inventory.salequotationcar"
"django_ledger.bankaccountmodel",
"django_ledger.chartofaccountmodel",
"django_ledger.estimatemodel",
"django_ledger.accountmodel",
"django_ledger.chartofaccountmodel",
"django_ledger.billmodel"
"django_ledger.itemmodel",
"django_ledger.invoicemodel",
"django_ledger.vendormodel",
"django_ledger.journalentrymodel"
]
permissions = cache.get(
"permissions_queryset",
Permission.objects.filter(
content_type__app_label__in=["inventory", "django_ledger"]
),
60 * 60,
content_type__app_label__in=[m.split('.')[0] for m in EXCLUDED_MODELS],
content_type__model__in=[m.split('.')[1] for m in EXCLUDED_MODELS]
).select_related('content_type')
)
# Group permissions by app_label and model
self.grouped_permissions = {}
for perm in permissions:
app_label = perm.content_type.app_label
model = perm.content_type.model
if app_label not in self.grouped_permissions:
self.grouped_permissions[app_label] = {}
if model not in self.grouped_permissions[app_label]:
self.grouped_permissions[app_label][model] = []
self.grouped_permissions[app_label][model].append(perm)
# Create a multiple choice field (hidden, will use custom rendering)
self.fields['permissions'] = forms.ModelMultipleChoiceField(
queryset=permissions,
widget=forms.MultipleHiddenInput(),
required=False,
initial=self.instance.permissions.all() if self.instance.pk else []
)
class Meta:
model = Permission
fields = ["name"]
fields = []
class UserGroupForm(forms.ModelForm):

View File

@ -1188,31 +1188,33 @@ class Staff(models.Model, LocalizedNameMixin):
return [x.customgroup for x in self.user.groups.all()]
def clear_groups(self):
EntityManagementModel.objects.filter(
user=self.user, entity=self.dealer.entity
).delete()
self.remove_superuser_permission()
return self.user.groups.clear()
def add_group(self, group):
def add_group(self, group,clean=False):
if clean:
self.clear_groups()
try:
self.user.groups.add(group)
if self.staff_type in ["accountant", "manager"]:
self.add_as_manager()
if "accountant" in group.name.lower() or "manager" in group.name.lower():
self.add_as_superuser()
except Exception as e:
print(e)
def add_as_manager(self):
if self.staff_type in ["accountant", "manager"]:
EntityManagementModel.objects.get_or_create(
user=self.user, entity=self.dealer.entity
)
else:
self.user.groups.clear()
group = Group.objects.filter(
customgroup__name__iexact=self.staff_type
).first()
if group:
self.add_group(group)
def add_as_superuser(self):
EntityManagementModel.objects.get_or_create(
user=self.user, entity=self.dealer.entity
)
def remove_superuser_permission(self):
EntityManagementModel.objects.filter(
user=self.user, entity=self.dealer.entity
).delete()
# self.user.groups.clear()
# group = Group.objects.filter(
# customgroup__name__iexact=self.staff_type
# ).first()
# if group:
# self.add_group(group)
class Meta:
verbose_name = _("Staff")
@ -2519,7 +2521,7 @@ class CustomGroup(models.Model):
@property
def entity(self):
return self.invoice.entity
return self.dealer.entity
@property
def users(self):

View File

@ -528,25 +528,25 @@ def track_lead_status_change(sender, instance, **kwargs):
pass # Ignore if the lead doesn't exist (e.g., during initial creation)
@receiver(post_save, sender=models.Lead)
def notify_assigned_staff(sender, instance, created, **kwargs):
"""
Signal handler that sends a notification to the staff member when a new lead is assigned.
This function is triggered when a Lead instance is saved. If the lead has been assigned
to a staff member, it creates a Notification object, notifying the staff member of the
new assignment.
# @receiver(post_save, sender=models.Lead)
# def notify_assigned_staff(sender, instance, created, **kwargs):
# """
# Signal handler that sends a notification to the staff member when a new lead is assigned.
# This function is triggered when a Lead instance is saved. If the lead has been assigned
# to a staff member, it creates a Notification object, notifying the staff member of the
# new assignment.
:param sender: The model class that sent the signal.
:param instance: The instance of the model that was saved.
:param created: A boolean indicating whether a new instance was created.
:param kwargs: Additional keyword arguments.
:return: None
"""
if instance.staff: # Check if the lead is assigned
models.Notification.objects.create(
user=instance.staff.staff_member.user,
message=f"You have been assigned a new lead: {instance.full_name}.",
)
# :param sender: The model class that sent the signal.
# :param instance: The instance of the model that was saved.
# :param created: A boolean indicating whether a new instance was created.
# :param kwargs: Additional keyword arguments.
# :return: None
# """
# if instance.staff: # Check if the lead is assigned
# models.Notification.objects.create(
# user=instance.staff.staff_member.user,
# message=f"You have been assigned a new lead: {instance.full_name}.",
# )
@receiver(post_save, sender=models.CarReservation)

View File

@ -644,4 +644,11 @@ def inventory_table(context, queryset):
"inventory_list": queryset,
}
ctx.update(queryset.aggregate(inventory_total_value=Sum("total_value")))
return ctx
return ctx
@register.filter
def count_checked(permissions):
"""Count how many permissions are marked as checked"""
print(permissions)
return sum(1 for perm in permissions if getattr(perm, 'is_checked', False))

View File

@ -913,7 +913,6 @@ urlpatterns = [
name="bill-action-force-migrate",
),
# path("items/bills/create/", views.bill_create, name="bill_create"),
# >>>>>>>>>>>>>>>>>>>>>>...
path(
"items/bills/<uuid:pk>/bill_detail/",
views.BillDetailView.as_view(),

View File

@ -318,6 +318,7 @@ def dealer_signup(request):
if password != password_confirm:
return JsonResponse({"error": _("Passwords do not match")}, status=400)
try:
#TODO make this a django-q task
create_user_dealer(
email, password, name, arabic_name, phone, crn, vrn, address
)
@ -2666,20 +2667,26 @@ class GroupCreateView(
success_message = _("Group created successfully")
def form_valid(self, form):
dealer = get_user_type(self.request)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
instance = form.save(commit=False)
group_name = f"{dealer.slug}_{instance.name}"
group,created = Group.objects.get_or_create(name=group_name)
try:
group, created = Group.objects.get_or_create(name=group_name)
instance.dealer = dealer
instance.group = group
instance.save()
except IntegrityError as e:
from django.utils.translation import gettext_lazy as _
print(e)
messages.error(self.request, _("Group name already exists"))
return redirect("group_create", dealer_slug=dealer.slug)
if created:
group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=dealer, group=group
group_manager, _ = models.CustomGroup.objects.get_or_create(
name=instance.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)
def get_success_url(self):
@ -2749,42 +2756,78 @@ def GroupDeleteview(request, dealer_slug,pk):
@login_required
def GroupPermissionView(request, dealer_slug,pk):
"""
Handles the view for adding or modifying permissions of a specific group. This view
fetches the group based on the primary key passed as a parameter, and either displays
a form for editing permissions or processes the submitted permissions.
def GroupPermissionView(request, dealer_slug, pk):
# Verify dealer and group exist
get_object_or_404(models.Dealer, slug=dealer_slug)
customgroup = get_object_or_404(models.CustomGroup, pk=pk)
If the request method is POST, the permissions of the group are cleared and updated
based on the submitted data. A success message is displayed upon completion, and
the user is redirected to the group's detail page.
In case of a GET request, the view renders the form pre-filled with the group's
current permissions.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key of the group whose permissions are being modified.
:type pk: int
:return: The HTTP response depending on the request type. For GET requests, renders
the permission form for the specified group. For POST requests, clears and updates
the group's permissions and redirects to the group's detail page.
:rtype: HttpResponse
"""
get_object_or_404(models.Dealer,slug=dealer_slug)
group = get_object_or_404(models.CustomGroup, pk=pk)
if request.method == "POST":
form = forms.PermissionForm(request.POST)
group.clear_permissions()
permissions = request.POST.getlist("name")
for i in permissions:
group.add_permission(Permission.objects.get(id=int(i)))
messages.success(request, _("Permission added successfully"))
return redirect("group_detail", dealer_slug=dealer_slug,pk=group.pk)
form = forms.PermissionForm(initial={"name": group.permissions})
return render(
request, "groups/group_permission_form.html", {"group": group, "form": form}
)
form = forms.PermissionForm(request.POST, instance=customgroup)
if form.is_valid():
# Clear existing permissions
customgroup.clear_permissions()
# Add new permissions from form
permissions = form.cleaned_data.get('permissions', [])
for permission in permissions:
customgroup.add_permission(permission)
messages.success(request, _("Permissions updated successfully"))
return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
else:
# Initial form with current permissions
form = forms.PermissionForm(instance=customgroup)
group_permission_ids = set(customgroup.permissions.values_list('id', flat=True))
# Mark permissions as checked in the form data
for app_label, model in form.grouped_permissions.items():
for mo, perms in model.items():
for perm in perms:
perm.is_checked = perm.id in group_permission_ids
return render(request,"groups/group_permission_form.html", {
"group": customgroup,
"form": form,
"group_permission_apps": set(customgroup.group.permissions.values_list('content_type__app_label', flat=True)),
"group_permission_models": set(customgroup.group.permissions.values_list('content_type__model', flat=True))
})
# def GroupPermissionView(request, dealer_slug,pk):
# """
# Handles the view for adding or modifying permissions of a specific group. This view
# fetches the group based on the primary key passed as a parameter, and either displays
# a form for editing permissions or processes the submitted permissions.
# If the request method is POST, the permissions of the group are cleared and updated
# based on the submitted data. A success message is displayed upon completion, and
# the user is redirected to the group's detail page.
# In case of a GET request, the view renders the form pre-filled with the group's
# current permissions.
# :param request: The HTTP request object.
# :type request: HttpRequest
# :param pk: The primary key of the group whose permissions are being modified.
# :type pk: int
# :return: The HTTP response depending on the request type. For GET requests, renders
# the permission form for the specified group. For POST requests, clears and updates
# the group's permissions and redirects to the group's detail page.
# :rtype: HttpResponse
# """
# get_object_or_404(models.Dealer,slug=dealer_slug)
# group = get_object_or_404(models.CustomGroup, pk=pk)
# if request.method == "POST":
# form = forms.PermissionForm(request.POST)
# group.clear_permissions()
# permissions = request.POST.getlist("name")
# for i in permissions:
# group.add_permission(Permission.objects.get(id=int(i)))
# messages.success(request, _("Permission added successfully"))
# return redirect("group_detail", dealer_slug=dealer_slug,pk=group.pk)
# form = forms.PermissionForm(initial={"name": group.permissions})
# return render(
# request, "groups/group_permission_form.html", {"group": group, "form": form}
# )
# Users
@ -2915,6 +2958,12 @@ class UserCreateView(
success_url = reverse_lazy("user_list")
success_message = _("User created successfully")
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
return form
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
# quota_dict = get_user_quota(dealer.user)
@ -2956,11 +3005,9 @@ class UserCreateView(
staff = form.save(commit=False)
staff.staff_member = staff_member
staff.dealer = dealer
staff.add_as_manager()
group = models.CustomGroup.objects.filter(dealer=dealer,name__iexact=staff.staff_type).first()
staff.save()
if group:
staff.add_group(group.group)
for customgroup in form.cleaned_data["group"]:
staff.add_group(customgroup.group)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("user_list", args=[self.request.dealer.slug])
@ -3004,30 +3051,32 @@ class UserUpdateView(
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
form.fields["email"].disabled = True
return form
def get_initial(self):
initial = super().get_initial()
initial["email"] = self.object.staff_member.user.email
initial["service_offered"] = self.object.staff_member.services_offered.all()
initial["group"] = self.object.groups
return initial
def form_valid(self, form):
services = form.cleaned_data["service_offered"]
if not services:
self.object.staff_member.services_offered.clear()
else:
for service in services:
self.object.staff_member.services_offered.add(service)
# services = form.cleaned_data["service_offered"]
# if not services:
# self.object.staff_member.services_offered.clear()
# else:
# for service in services:
# self.object.staff_member.services_offered.add(service)
staff = form.save(commit=False)
staff.name = form.cleaned_data["name"]
staff.arabic_name = form.cleaned_data["arabic_name"]
staff.phone_number = form.cleaned_data["phone_number"]
staff.staff_type = form.cleaned_data["staff_type"]
staff.add_as_manager()
for customgroup in form.cleaned_data["group"]:
staff.add_group(customgroup.group,True)
staff.save()
return super().form_valid(form)
def get_success_url(self):
@ -5180,7 +5229,7 @@ def lead_create(request,dealer_slug):
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = dealer
instance.staff = form.cleaned_data.get("staff")
# instance.staff = form.cleaned_data.get("staff")
if instance.lead_type == "customer":
customer = models.Customer.objects.filter(
@ -5252,12 +5301,12 @@ def lead_create(request,dealer_slug):
is_sa_import=True, pk__in=dealer_make_list
)
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(
dealer=dealer, staff_type="sales"
dealer=dealer
)
if hasattr(request.user.staffmember, "staff"):
form.initial["staff"] = request.user.staffmember.staff
form.fields["staff"].widget.attrs["disabled"] = True
form.fields["staff"].widget = HiddenInput()
form.fields["id_car_make"].queryset = qs
form.fields["id_car_make"].choices = [
(obj.id_car_make, obj.get_local_name()) for obj in qs
@ -5817,6 +5866,13 @@ class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin)
initial["stage"] = models.Stage.QUALIFICATION
return initial
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer)
form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer)
return form
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
instance = form.save(commit=False)
@ -5859,6 +5915,13 @@ class OpportunityUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView)
template_name = "crm/opportunities/opportunity_form.html"
success_message = "Opportunity updated successfully."
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer)
form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer)
return form
def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
@ -8787,8 +8850,7 @@ def payment_callback(request,dealer_slug):
return render(request, "payment_failed.html", {"message": message})
def sse_stream(request):
print("hi")
def sse_stream(request):
def event_stream():
last_id = request.GET.get("last_id", 0)
while True:

View File

@ -11,7 +11,8 @@
<div class="col-auto">
<div class="d-md-flex justify-content-between">
<div>
<a href="{% url 'group_create' request.dealer.slug %}" class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{% trans "Add Group" %}</a>
<a href="{% url 'group_create' request.dealer.slug %}" class="btn btn-sm btn-phoenix-primary me-5"><span class="fas fa-plus me-2"></span>{% trans "Add Group" %}</a>
<a href="{% url 'user_list' request.dealer.slug %}" class="btn btn-sm btn-phoenix-secondary"><span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}</a>
</div>
</div>

View File

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block title %}{% trans "Permission" %}{% endblock title %}
{% block content %}
<div class="row">
<div class="row">
<div class="col-sm-9">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if group.created %}
{{ _("Edit Permission") }}
{% else %}
{{ _("Add Permission") }}
{% endif %}
</h3>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-9">
<form class="row g-3 mb-9" method="post" class="form" novalidate>
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% for error in form.errors %}
<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">
<i class="fa-solid fa-floppy-disk"></i>
{{ _("Save") }}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,46 +1,157 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block title %}{% trans "Permission" %}{% endblock title %}
{% load custom_filters %}
{% block title %}{% trans "Permission Management" %}{% endblock title %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<div class="d-sm-flex justify-content-between align-items-center">
<h3 class="mb-0">
{% if group %}
{{ _("Edit Permissions for") }}: <strong>{{ group.name }}</strong>
{% else %}
{{ _("Add Permissions") }}
{% endif %}
</h3>
<div class="row">
<div class="row">
<div class="col-sm-9">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if group.created %}
{{ _("Edit Permission") }}
{% else %}
{{ _("Add Permission") }}
{% endif %}
</h3>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-9">
</div>
<form class="row g-3 mb-9" method="post" class="form" novalidate>
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% for error in form.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex mb-3">
<button class="btn btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
{{ _("Save") }}
<form method="post" novalidate>
{% csrf_token %}
<!-- Permissions Grid -->
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
{% for app_label, models in form.grouped_permissions.items %}
<div class="col">
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
{{ app_label|capfirst }}
</h5>
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
{{ models|length }} {% trans "models" %}
</span>
</div>
</div>
<div class="card-body">
<div class="accordion">
{% for model, perms in models.items %}
<div class="accordion-item border-0">
<h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<button class="accordion-button collapsed bg-white shadow-none py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
aria-expanded="false">
<i class="fas fa-table me-2"></i>{{ model|capfirst }}
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
{{ perms|length }} / {{ perms|count_checked }}
</span>
</button>
</h6>
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
class="accordion-collapse collapse"
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<div class="accordion-body pt-0">
<div class="list-group list-group-flush">
{% for perm in perms %}
<label class="list-group-item d-flex gap-2 {% if perm.is_checked %}bg-light-primary{% endif %}">
<input class="form-check-input flex-shrink-0 mt-0"
type="checkbox"
name="permissions"
value="{{ perm.id }}"
id="perm_{{ perm.id }}"
{% if perm.is_checked %}checked{% endif %}>
<span>
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
<small class="d-block text-muted">{{ perm.codename }}</small>
{% if perm.is_checked %}
<span class="badge bg-success mt-1">
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
</span>
{% endif %}
</span>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-4">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary rounded-pill me-2">
{{ group.permissions.count }} {% trans "selected" %}
</span>
<span class="text-muted">
{% trans "Permissions will be updated immediately" %}
</span>
</div>
<div>
<a href="{% url 'group_detail' request.dealer.slug group.pk %}"
class="btn btn-outline-secondary me-2">
<i class="fas fa-ban me-2"></i>{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>{% trans "Save Changes" %}
</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>
</div>
</div>
</div>
</form>
</div>
<style>
.bg-light-primary {
background-color: rgba(13, 110, 253, 0.1);
}
.list-group-item:hover {
background-color: rgba(0, 0, 0, 0.03);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Search functionality
document.getElementById('permissionSearch').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
document.querySelectorAll('.accordion-body .list-group-item').forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(searchTerm) ? '' : 'none';
});
// Open relevant accordions
document.querySelectorAll('.accordion-collapse').forEach(collapse => {
const visibleItems = collapse.querySelectorAll('.list-group-item[style=""]');
if (visibleItems.length > 0) {
const button = document.querySelector(
`button[data-bs-target="#${collapse.id}"]`
);
if (button && !button.classList.contains('collapsed')) {
new bootstrap.Collapse(collapse, {toggle: true});
}
}
});
});
});
</script>
{% endblock %}

View File

@ -81,10 +81,6 @@
{{ _("Sale Order")}} #{{ saleorder.formatted_order_id }}
</h1>
<div>
<<<<<<< HEAD
=======
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
<button class="btn btn-sm btn-outline-light me-2">
<i class="fas fa-print me-1"></i> {{ _("Print") }}
</button>
@ -275,18 +271,10 @@
<!-- Documents Card -->
<div class="card mb-4 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<<<<<<< HEAD
<h5 class="mb-0">{{ _("Documents") }}</h5>
<button class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i> {{ _("Add Document")}}
=======
<h5 class="mb-0">{{ _("Documents") }}</h5>
<button class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i> {{ _("Add Document")}}
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</button>
</button>
</div>
<div class="card-body">
<div class="file-upload mb-3">
@ -330,13 +318,7 @@
<div class="mb-3">
<textarea class="form-control" name="comment" rows="3" placeholder="Add a comment or note..." required></textarea>
<div class="d-flex justify-content-end mt-2">
<<<<<<< HEAD
<button type="submit" class="btn btn-primary btn-sm">{{ _("Post Comment")}}</button>
=======
<button type="submit" class="btn btn-primary btn-sm">{{ _("Post Comment")}}</button>
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</div>
</div>
</form>
@ -379,52 +361,27 @@
{% endif %}
{% comment %} <a href="{% url 'edit_sale_order' saleorder.pk %}" class="btn btn-primary"> {% endcomment %}
<<<<<<< HEAD
=======
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
<a href="" class="btn btn-primary">
<i class="fas fa-edit me-2"></i> {{ _("Edit Order")}}
</a>
{% if not saleorder.invoice %}
{% comment %} <a href="{% url 'create_invoice_from_order' saleorder.pk %}" class="btn btn-info"> {% endcomment %}
<<<<<<< HEAD
<a href="" class="btn btn-info">
<i class="fas fa-file-invoice-dollar me-2"></i> {{ _("Create Invoice")}}
=======
<a href="" class="btn btn-info">
<i class="fas fa-file-invoice-dollar me-2"></i> {{ _("Create Invoice")}}
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</a>
</a>
{% endif %}
{% if saleorder.status == 'approved' and not saleorder.actual_delivery_date %}
<<<<<<< HEAD
<button class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#deliveryModal">
<i class="fas fa-truck me-2"></i> {{ _("Schedule Delivery")}}
=======
<button class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#deliveryModal">
<i class="fas fa-truck me-2"></i> {{ _("Schedule Delivery")}}
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</button>
</button>
{% endif %}
{% if saleorder.status != 'cancelled' %}
<<<<<<< HEAD
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
<i class="fas fa-times-circle me-2"></i> {{ _("Cancel Order")}}
=======
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
<i class="fas fa-times-circle me-2"></i> {{ _("Cancel Order")}}
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</button>
</button>
{% endif %}
</div>
</div>
@ -575,15 +532,8 @@
</div>
</div>
<div class="modal-footer">
<<<<<<< HEAD
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-danger">{{ _("Confirm Cancellation")}}</button>
=======
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-danger">{{ _("Confirm Cancellation")}}</button>
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</div>
</form>
</div>
@ -612,15 +562,8 @@
</div>
</div>
<div class="modal-footer">
<<<<<<< HEAD
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-primary">{{ _("Schedule Delivery")}}</button>
=======
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-primary">{{ _("Schedule Delivery")}}</button>
>>>>>>> e9e2fd3 (add bulk insert + po item insert)
</div>
</form>
</div>

View File

@ -18,7 +18,15 @@
</div>
<div class="col-md-6">
<p><strong>{{ _("Phone Number") }}:</strong> {{ user_.phone_number }}</p>
<p><strong>{{ _("Role") }}:</strong> {{ user_.staff_type }}</p>
<div>
<span><strong>{{ _("Roles") }}:</strong></span>
{% for group in user_.groups %}
<span><strong>{{ group.name }}</strong></span>
{% if not forloop.last %}
<span>&amp;</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>

View File

@ -39,8 +39,7 @@
{{ form.arabic_name|as_crispy_field }}
{{ form.email|as_crispy_field }}
{{ form.phone_number|as_crispy_field }}
{{ form.staff_type|as_crispy_field }}
{{ form.service_offered|as_crispy_field }}
{{ form.group|as_crispy_field }}
{% for error in form.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
@ -55,4 +54,5 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -6,7 +6,6 @@
{% block content %}
<section class="">
<div class="row mt-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
@ -47,7 +46,10 @@
<td class="align-middle white-space-nowrap align-items-center">{{ user.email }}</td>
<td class="align-middle white-space-nowrap align-items-center justify-content-center">{{ user.phone_number }}</td>
<td>
<span class="badge badge-sm bg-primary text-center"><i class="fa-solid fa-scroll"></i> {% trans user.staff_type|title %}</span>
{% for group in user.groups %}
<span class="badge badge-sm bg-primary text-center"><i class="fa-solid fa-scroll"></i> {% trans group.name|title %}</span>
{% endfor %}
</td>
<td class="align-middle white-space-nowrap">