indexing and query optimization
This commit is contained in:
parent
db751f7837
commit
0bc0d17f43
@ -4,6 +4,8 @@ from django.conf.urls.static import static
|
||||
from django.conf import settings
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from inventory import views
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
|
||||
# import debug_toolbar
|
||||
from schema_graph.views import Schema
|
||||
@ -15,7 +17,7 @@ urlpatterns = [
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
path("api/", include("api.urls")),
|
||||
# path('dj-rest-auth/', include('dj_rest_auth.urls')),
|
||||
]
|
||||
] + debug_toolbar_urls()
|
||||
urlpatterns += i18n_patterns(
|
||||
path("admin/", admin.site.urls),
|
||||
path("switch_language/", views.switch_language, name="switch_language"),
|
||||
@ -32,3 +34,5 @@ urlpatterns += i18n_patterns(
|
||||
)
|
||||
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
|
||||
|
||||
BIN
dbtest.sqlite3
BIN
dbtest.sqlite3
Binary file not shown.
@ -43,6 +43,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from appointment.models import StaffMember
|
||||
from plans.quota import get_user_quota
|
||||
from plans.models import UserPlan
|
||||
from django.db.models import Q
|
||||
# from plans.models import AbstractPlan
|
||||
# from simple_history.models import HistoricalRecords
|
||||
|
||||
@ -243,6 +244,11 @@ class CarMake(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Make")
|
||||
indexes = [
|
||||
models.Index(fields=['name'], name='car_make_name_idx'),
|
||||
models.Index(fields=['is_sa_import'], name='car_make_sa_import_idx'),
|
||||
models.Index(fields=['car_type'], name='car_make_type_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarModel(models.Model, LocalizedNameMixin):
|
||||
@ -272,6 +278,11 @@ class CarModel(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Model")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_make'], name='car_model_make_idx'),
|
||||
models.Index(fields=['name'], name='car_model_name_idx'),
|
||||
models.Index(fields=['id_car_make', 'name'], name='car_model_make_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarSerie(models.Model, LocalizedNameMixin):
|
||||
@ -306,6 +317,12 @@ class CarSerie(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Series")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_model'], name='car_serie_model_idx'),
|
||||
models.Index(fields=['year_begin', 'year_end'], name='car_serie_years_idx'),
|
||||
models.Index(fields=['name'], name='car_serie_name_idx'),
|
||||
models.Index(fields=['generation_name'], name='car_serie_generation_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarTrim(models.Model, LocalizedNameMixin):
|
||||
@ -339,6 +356,11 @@ class CarTrim(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Trim")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_serie'], name='car_trim_serie_idx'),
|
||||
models.Index(fields=['start_production_year', 'end_production_year'], name='car_trim_prod_years_idx'),
|
||||
models.Index(fields=['name'], name='car_trim_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarEquipment(models.Model, LocalizedNameMixin):
|
||||
@ -359,6 +381,11 @@ class CarEquipment(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Equipment")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_trim'], name='car_equipment_trim_idx'),
|
||||
models.Index(fields=['year_begin'], name='car_equipment_year_idx'),
|
||||
models.Index(fields=['name'], name='car_equipment_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarSpecification(models.Model, LocalizedNameMixin):
|
||||
@ -390,6 +417,10 @@ class CarSpecification(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Specification")
|
||||
indexes = [
|
||||
models.Index(fields=['id_parent'], name='car_spec_parent_idx'),
|
||||
models.Index(fields=['name'], name='car_spec_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarSpecificationValue(models.Model):
|
||||
@ -406,6 +437,11 @@ class CarSpecificationValue(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Specification Value")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_trim'], name='car_spec_val_trim_idx'),
|
||||
models.Index(fields=['id_car_specification'], name='car_spec_val_spec_idx'),
|
||||
models.Index(fields=['id_car_trim', 'id_car_specification'], name='car_spec_val_trim_spec_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarOption(models.Model, LocalizedNameMixin):
|
||||
@ -437,6 +473,10 @@ class CarOption(models.Model, LocalizedNameMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Option")
|
||||
indexes = [
|
||||
models.Index(fields=['id_parent'], name='car_option_parent_idx'),
|
||||
models.Index(fields=['name'], name='car_option_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarOptionValue(models.Model):
|
||||
@ -456,6 +496,12 @@ class CarOptionValue(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Option Value")
|
||||
indexes = [
|
||||
models.Index(fields=['id_car_option'], name='car_opt_val_option_idx'),
|
||||
models.Index(fields=['id_car_equipment'], name='car_opt_val_equipment_idx'),
|
||||
models.Index(fields=['is_base'], name='car_opt_val_is_base_idx'),
|
||||
models.Index(fields=['id_car_option', 'id_car_equipment'], name='cov_option_equipment_idx'),
|
||||
]
|
||||
|
||||
|
||||
class CarTransferStatusChoices(models.TextChoices):
|
||||
@ -614,6 +660,26 @@ class Car(Base):
|
||||
class Meta:
|
||||
verbose_name = _("Car")
|
||||
verbose_name_plural = _("Cars")
|
||||
indexes = [
|
||||
models.Index(fields=['vin'], name='car_vin_idx'),
|
||||
models.Index(fields=['year'], name='car_year_idx'),
|
||||
models.Index(fields=['status'], name='car_status_idx'),
|
||||
|
||||
models.Index(fields=['dealer'], name='car_dealer_idx'),
|
||||
models.Index(fields=['vendor'], name='car_vendor_idx'),
|
||||
models.Index(fields=['id_car_make'], name='car_make_idx'),
|
||||
models.Index(fields=['id_car_model'], name='car_model_idx'),
|
||||
models.Index(fields=['id_car_serie'], name='car_serie_idx'),
|
||||
models.Index(fields=['id_car_trim'], name='car_trim_idx'),
|
||||
|
||||
models.Index(fields=['id_car_make', 'id_car_model'], name='car_make_model_idx'),
|
||||
models.Index(fields=['id_car_make', 'year'], name='car_make_year_idx'),
|
||||
models.Index(fields=['dealer', 'status'], name='car_dealer_status_idx'),
|
||||
models.Index(fields=['vendor', 'status'], name='car_vendor_status_idx'),
|
||||
models.Index(fields=['year', 'status'], name='car_year_status_idx'),
|
||||
models.Index(fields=['status'], name='car_active_status_idx',
|
||||
condition=Q(status=CarStatusChoices.AVAILABLE)),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
make = self.id_car_make.name if self.id_car_make else "Unknown Make"
|
||||
@ -891,6 +957,12 @@ class CarFinance(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Car Financial Details")
|
||||
verbose_name_plural = _("Car Financial Details")
|
||||
indexes = [
|
||||
models.Index(fields=['car'], name='car_finance_car_idx'),
|
||||
models.Index(fields=['cost_price'], name='car_finance_cost_price_idx'),
|
||||
models.Index(fields=['selling_price'], name='car_finance_selling_price_idx'),
|
||||
models.Index(fields=['discount_amount'], name='car_finance_discount_idx'),
|
||||
]
|
||||
|
||||
|
||||
class ExteriorColors(models.Model, LocalizedNameMixin):
|
||||
@ -901,6 +973,10 @@ class ExteriorColors(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Exterior Colors")
|
||||
verbose_name_plural = _("Exterior Colors")
|
||||
indexes = [
|
||||
models.Index(fields=['name'], name='exterior_color_name_idx'),
|
||||
models.Index(fields=['arabic_name'], name='exterior_color_arabic_name_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.rgb})"
|
||||
@ -914,6 +990,10 @@ class InteriorColors(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Interior Colors")
|
||||
verbose_name_plural = _("Interior Colors")
|
||||
indexes = [
|
||||
models.Index(fields=['name'], name='interior_color_name_idx'),
|
||||
models.Index(fields=['arabic_name'], name='interior_color_arabic_name_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.rgb})"
|
||||
@ -932,6 +1012,11 @@ class CarColors(models.Model):
|
||||
verbose_name = _("Color")
|
||||
verbose_name_plural = _("Colors")
|
||||
unique_together = ("car", "exterior", "interior")
|
||||
indexes = [
|
||||
models.Index(fields=['exterior'], name='car_colors_exterior_idx'),
|
||||
models.Index(fields=['interior'], name='car_colors_interior_idx'),
|
||||
models.Index(fields=['exterior', 'interior'], name='car_colors_ext_int_combo_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
|
||||
@ -1108,6 +1193,7 @@ class Dealer(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Dealer")
|
||||
verbose_name_plural = _("Dealers")
|
||||
indexes = [models.Index(fields=["name"])]
|
||||
# permissions = [
|
||||
# ('change_dealer_type', 'Can change dealer type'),
|
||||
# ]
|
||||
@ -1188,7 +1274,7 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
return CustomGroup.objects.filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
|
||||
return CustomGroup.objects.select_related("group").filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
|
||||
|
||||
def clear_groups(self):
|
||||
self.remove_superuser_permission()
|
||||
@ -1217,6 +1303,10 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Staff")
|
||||
verbose_name_plural = _("Staff")
|
||||
indexes = [
|
||||
models.Index(fields=["name"]),
|
||||
models.Index(fields=["staff_type"]),
|
||||
]
|
||||
permissions = []
|
||||
|
||||
def __str__(self):
|
||||
@ -1372,6 +1462,13 @@ class Customer(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Customer")
|
||||
verbose_name_plural = _("Customers")
|
||||
indexes = [
|
||||
models.Index(fields=["title"]),
|
||||
models.Index(fields=["first_name"]),
|
||||
models.Index(fields=["last_name"]),
|
||||
models.Index(fields=["email"]),
|
||||
models.Index(fields=["phone_number"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
# middle = f" {self.middle_name}" if self.middle_name else ""
|
||||
@ -1510,6 +1607,11 @@ class Organization(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Organization")
|
||||
verbose_name_plural = _("Organizations")
|
||||
indexes = [
|
||||
models.Index(fields=["name"]),
|
||||
models.Index(fields=["email"]),
|
||||
models.Index(fields=["phone_number"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -1699,6 +1801,17 @@ class Lead(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Lead")
|
||||
verbose_name_plural = _("Leads")
|
||||
indexes = [
|
||||
models.Index(fields=["dealer"]),
|
||||
models.Index(fields=["customer"]),
|
||||
models.Index(fields=["organization"]),
|
||||
models.Index(fields=["staff"]),
|
||||
models.Index(fields=["first_name"]),
|
||||
models.Index(fields=["last_name"]),
|
||||
models.Index(fields=["email"]),
|
||||
models.Index(fields=["phone_number"]),
|
||||
models.Index(fields=["created"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
@ -1873,6 +1986,14 @@ class Schedule(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ["-scheduled_at"]
|
||||
verbose_name = _("Schedule")
|
||||
verbose_name_plural = _("Schedules")
|
||||
indexes = [
|
||||
models.Index(fields=["dealer"]),
|
||||
models.Index(fields=["customer"]),
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
models.Index(fields=["scheduled_at"]),
|
||||
]
|
||||
|
||||
|
||||
class LeadStatusHistory(models.Model):
|
||||
@ -2045,6 +2166,14 @@ class Opportunity(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Opportunity")
|
||||
verbose_name_plural = _("Opportunities")
|
||||
indexes = [
|
||||
models.Index(fields=["dealer"]),
|
||||
models.Index(fields=["customer"]),
|
||||
models.Index(fields=["car"]),
|
||||
models.Index(fields=["lead"]),
|
||||
models.Index(fields=["organization"]),
|
||||
models.Index(fields=["created"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
if self.customer:
|
||||
@ -2069,6 +2198,19 @@ class Notes(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Note")
|
||||
verbose_name_plural = _("Notes")
|
||||
indexes = [
|
||||
models.Index(fields=['dealer'], name='note_dealer_idx'),
|
||||
models.Index(fields=['created_by'], name='note_created_by_idx'),
|
||||
models.Index(fields=['content_type'], name='note_content_type_idx'),
|
||||
models.Index(fields=['content_type', 'object_id'], name='note_content_object_idx'),
|
||||
|
||||
models.Index(fields=['created'], name='note_created_date_idx'),
|
||||
models.Index(fields=['updated'], name='note_updated_date_idx'),
|
||||
|
||||
models.Index(fields=['dealer', 'created'], name='note_dealer_created_idx'),
|
||||
models.Index(fields=['content_type', 'object_id', 'created'],
|
||||
name='note_content_obj_created_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Note by {self.created_by.first_name} on {self.content_object}"
|
||||
@ -2099,6 +2241,19 @@ class Tasks(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Task")
|
||||
verbose_name_plural = _("Tasks")
|
||||
indexes = [
|
||||
models.Index(fields=['dealer'], name='task_dealer_idx'),
|
||||
models.Index(fields=['created_by'], name='task_created_by_idx'),
|
||||
models.Index(fields=['content_type'], name='task_content_type_idx'),
|
||||
models.Index(fields=['content_type', 'object_id'], name='task_content_object_idx'),
|
||||
|
||||
models.Index(fields=['created'], name='task_created_date_idx'),
|
||||
models.Index(fields=['updated'], name='task_updated_date_idx'),
|
||||
|
||||
models.Index(fields=['dealer', 'created'], name='task_dealer_created_idx'),
|
||||
models.Index(fields=['content_type', 'object_id', 'created'],
|
||||
name='task_content_obj_created_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Task by {self.created_by.email} on {self.content_object}"
|
||||
@ -2127,6 +2282,17 @@ class Email(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Email")
|
||||
verbose_name_plural = _("Emails")
|
||||
indexes = [
|
||||
models.Index(fields=['created_by'], name='email_created_by_idx'),
|
||||
models.Index(fields=['content_type'], name='email_content_type_idx'),
|
||||
models.Index(fields=['content_type', 'object_id'], name='email_content_object_idx'),
|
||||
|
||||
models.Index(fields=['created'], name='email_created_date_idx'),
|
||||
models.Index(fields=['updated'], name='email_updated_date_idx'),
|
||||
|
||||
models.Index(fields=['content_type', 'object_id', 'created'],
|
||||
name='email_content_obj_created_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Email by {self.created_by.first_name} on {self.content_object}"
|
||||
@ -2152,6 +2318,17 @@ class Activity(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Activity")
|
||||
verbose_name_plural = _("Activities")
|
||||
indexes = [
|
||||
models.Index(fields=['created_by'], name='activity_created_by_idx'),
|
||||
models.Index(fields=['content_type'], name='activity_content_type_idx'),
|
||||
models.Index(fields=['content_type', 'object_id'], name='activity_content_object_idx'),
|
||||
|
||||
models.Index(fields=['created'], name='activity_created_date_idx'),
|
||||
models.Index(fields=['updated'], name='activity_updated_date_idx'),
|
||||
|
||||
models.Index(fields=['content_type', 'object_id', 'created'],
|
||||
name='a_content_obj_created_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_activity_type_display()} by {self.created_by.get_full_name} on {self.content_object}"
|
||||
@ -2170,6 +2347,12 @@ class Notification(models.Model):
|
||||
verbose_name_plural = _("Notifications")
|
||||
ordering = ["-created"]
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=['user'], name='notification_user_idx'),
|
||||
models.Index(fields=['is_read'], name='notification_is_read_idx'),
|
||||
models.Index(fields=['created'], name='notification_created_date_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
@ -2233,6 +2416,12 @@ class Vendor(models.Model, LocalizedNameMixin):
|
||||
class Meta:
|
||||
verbose_name = _("Vendor")
|
||||
verbose_name_plural = _("Vendors")
|
||||
indexes = [
|
||||
models.Index(fields=['slug'], name='vendor_slug_idx'),
|
||||
models.Index(fields=['active'], name='vendor_active_idx'),
|
||||
models.Index(fields=['crn'], name='vendor_crn_idx'),
|
||||
models.Index(fields=['vrn'], name='vendor_vrn_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -2474,9 +2663,21 @@ class SaleOrder(models.Model):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Sales Order"
|
||||
verbose_name_plural = "Sales Orders"
|
||||
verbose_name = _("Sales Order")
|
||||
verbose_name_plural = _("Sales Orders")
|
||||
ordering = ["-order_date"] # Order by most recent first
|
||||
indexes = [
|
||||
models.Index(fields=["dealer"]),
|
||||
models.Index(fields=["estimate"]),
|
||||
models.Index(fields=["invoice"]),
|
||||
models.Index(fields=["opportunity"]),
|
||||
models.Index(fields=["customer"]),
|
||||
models.Index(fields=["status"]),
|
||||
models.Index(fields=["order_date"]),
|
||||
models.Index(fields=["expected_delivery_date"]),
|
||||
models.Index(fields=["actual_delivery_date"]),
|
||||
models.Index(fields=["cancelled_date"]),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.formatted_order_id:
|
||||
@ -2534,6 +2735,14 @@ class CustomGroup(models.Model):
|
||||
group = models.OneToOneField(
|
||||
"auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE
|
||||
)
|
||||
class Meta:
|
||||
verbose_name = _("Custom Group")
|
||||
verbose_name_plural = _("Custom Groups")
|
||||
indexes = [
|
||||
models.Index(fields=["name"]),
|
||||
models.Index(fields=["dealer"]),
|
||||
models.Index(fields=["group"]),
|
||||
]
|
||||
|
||||
@property
|
||||
def entity(self):
|
||||
@ -2669,7 +2878,7 @@ class CustomGroup(models.Model):
|
||||
app="inventory",
|
||||
allowed_models=[
|
||||
"saleorder",
|
||||
"payment",
|
||||
# "payment",
|
||||
"staff",
|
||||
"schedule",
|
||||
"activity",
|
||||
@ -2939,6 +3148,13 @@ class PoItemsUploaded(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("PO Items")
|
||||
verbose_name_plural = _("PO Items")
|
||||
indexes = [
|
||||
models.Index(fields=["po"]),
|
||||
models.Index(fields=["item"]),
|
||||
]
|
||||
def get_name(self):
|
||||
return self.item.item.name.split('||')
|
||||
class ExtraInfo(models.Model):
|
||||
@ -2990,7 +3206,9 @@ class ExtraInfo(models.Model):
|
||||
models.Index(fields=['content_type', 'object_id']),
|
||||
models.Index(fields=['related_content_type', 'related_object_id']),
|
||||
]
|
||||
verbose_name_plural = "Extra Info"
|
||||
verbose_name_plural = _("Extra Info")
|
||||
verbose_name = _("Extra Info")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"ExtraInfo for {self.content_object} ({self.content_type})"
|
||||
|
||||
@ -316,12 +316,18 @@ class BasePurchaseOrderActionActionView(LoginRequiredMixin,
|
||||
)
|
||||
except ValidationError as e:
|
||||
# --- Single-line log for ValidationError ---
|
||||
print(f"User {user_username} encountered a validation error "
|
||||
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
|
||||
f"Error: {e}")
|
||||
logger.warning(
|
||||
f"User {user_username} encountered a validation error "
|
||||
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
|
||||
f"Error: {e}"
|
||||
)
|
||||
except AttributeError as e:
|
||||
print(f"User {user_username} encountered an AttributeError "
|
||||
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
|
||||
f"Error: {e}")
|
||||
logger.warning(
|
||||
f"User {user_username} encountered an AttributeError "
|
||||
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
|
||||
@ -421,11 +427,16 @@ class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
|
||||
user_model=dealer.entity.admin,
|
||||
instance=self.object
|
||||
)
|
||||
return form_class(
|
||||
form = form_class(
|
||||
entity_model=entity_model,
|
||||
user_model=dealer.entity.admin,
|
||||
**self.get_form_kwargs()
|
||||
)
|
||||
try:
|
||||
form.initial['amount_paid'] = self.object.get_itemtxs_data()[1]["total_amount__sum"]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return form
|
||||
|
||||
def get_form_class(self):
|
||||
bill_model: BillModel = self.object
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
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
|
||||
from django.dispatch import receiver
|
||||
from appointment.models import Service
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth import get_user_model
|
||||
@ -941,6 +941,11 @@ def create_po_item_upload(sender,instance,created,**kwargs):
|
||||
dealer = models.Dealer.objects.get(entity=instance.entity)
|
||||
models.PoItemsUploaded.objects.create(dealer=dealer,po=instance, item=item, status="fulfilled")
|
||||
|
||||
@receiver(post_save, sender=models.Staff)
|
||||
def add_service_to_staff(sender,instance,created,**kwargs):
|
||||
if created:
|
||||
for service in Service.objects.all():
|
||||
instance.staff_member.services_offered.add(service)
|
||||
|
||||
##########################################################
|
||||
######################Notification########################
|
||||
|
||||
@ -473,7 +473,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
|
||||
calculator = CarFinanceCalculator(invoice)
|
||||
finance_data = calculator.get_finance_data()
|
||||
|
||||
# handle_account_process(invoice, amount, finance_data)
|
||||
handle_account_process(invoice, amount, finance_data)
|
||||
invoice.make_payment(amount)
|
||||
invoice.save()
|
||||
|
||||
|
||||
@ -206,7 +206,7 @@ from .tasks import create_accounts_for_make, create_user_dealer, send_email
|
||||
# djago easy audit log
|
||||
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
|
||||
from django_q.tasks import async_task
|
||||
|
||||
from django.db.models import Prefetch
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@ -1142,7 +1142,7 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
dealer = get_user_type(self.request)
|
||||
qs = super().get_queryset()
|
||||
qs = super().get_queryset().prefetch_related("id_car_make", "id_car_model", "id_car_trim","finances","colors")
|
||||
qs = qs.filter(dealer=dealer)
|
||||
status = self.request.GET.get("status")
|
||||
search = self.request.GET.get("search")
|
||||
@ -1194,8 +1194,193 @@ def inventory_stats_view(request, dealer_slug):
|
||||
"inventory/inventory_stats.html" template.
|
||||
:rtype: HttpResponse
|
||||
"""
|
||||
# cars = (
|
||||
# models.Car.objects
|
||||
# .filter(dealer=request.dealer)
|
||||
# .select_related(
|
||||
# "id_car_make",
|
||||
# "id_car_model",
|
||||
# "id_car_trim__id_car_serie__id_car_model__id_car_make"
|
||||
# )
|
||||
# .only(
|
||||
# "id",
|
||||
# "id_car_make__id_car_make",
|
||||
# "id_car_make__slug",
|
||||
# "id_car_make__name",
|
||||
# "id_car_make__arabic_name",
|
||||
# "id_car_model__id_car_model",
|
||||
# "id_car_model__slug",
|
||||
# "id_car_model__name",
|
||||
# "id_car_model__arabic_name",
|
||||
# "id_car_trim__id_car_trim",
|
||||
# "id_car_trim__slug",
|
||||
# "id_car_trim__name",
|
||||
# "id_car_trim__id_car_serie__id_car_model__id_car_make",
|
||||
# "id_car_trim__id_car_serie__id_car_model__id_car_model",
|
||||
# )
|
||||
# .order_by('id_car_make', 'id_car_model', 'id_car_trim')
|
||||
# )
|
||||
|
||||
# Base queryset for cars belonging to the dealer
|
||||
# # Get counts in optimized queries
|
||||
# total_cars = cars.count()
|
||||
# reserved_cars = models.CarReservation.objects.filter(
|
||||
# car__dealer=request.dealer
|
||||
# ).count()
|
||||
|
||||
# # Prefetch related data if needed for additional fields
|
||||
# cars = cars.prefetch_related(
|
||||
# Prefetch('colors', queryset=models.CarColors.objects.select_related('exterior', 'interior'))
|
||||
# )
|
||||
|
||||
# # Get distinct makes, models, trims in database-compatible way
|
||||
# makes = (
|
||||
# cars.order_by('id_car_make')
|
||||
# .values_list('id_car_make', flat=True)
|
||||
# .distinct()
|
||||
# )
|
||||
|
||||
# _models = (
|
||||
# cars.order_by('id_car_model')
|
||||
# .values_list('id_car_model', flat=True)
|
||||
# .distinct()
|
||||
# )
|
||||
|
||||
# trims = (
|
||||
# cars.order_by('id_car_trim')
|
||||
# .values_list('id_car_trim', flat=True)
|
||||
# .distinct()
|
||||
# )
|
||||
|
||||
# # Get counts by make/model/trim
|
||||
# make_counts = dict(
|
||||
# cars.values_list('id_car_make')
|
||||
# .annotate(count=Count('id'))
|
||||
# .order_by('id_car_make')
|
||||
# )
|
||||
|
||||
# model_counts = dict(
|
||||
# cars.values_list('id_car_model')
|
||||
# .annotate(count=Count('id'))
|
||||
# .order_by('id_car_model')
|
||||
# )
|
||||
|
||||
# trim_counts = dict(
|
||||
# cars.values_list('id_car_trim')
|
||||
# .annotate(count=Count('id'))
|
||||
# .order_by('id_car_trim')
|
||||
# )
|
||||
|
||||
# # Build inventory structure
|
||||
# inventory = {}
|
||||
|
||||
# # Process makes
|
||||
# make_objects = {
|
||||
# m.id_car_make: m for m in
|
||||
# models.CarMake.objects.filter(id_car_make__in=makes)
|
||||
# .only('id_car_make', 'slug', 'name', 'arabic_name')
|
||||
# }
|
||||
|
||||
# for make_id in makes:
|
||||
# if not make_id:
|
||||
# continue
|
||||
|
||||
# make_obj = make_objects.get(make_id)
|
||||
# if not make_obj:
|
||||
# continue
|
||||
|
||||
# inventory[make_id] = {
|
||||
# "make_id": make_id,
|
||||
# "slug": make_obj.slug,
|
||||
# "make_name": make_obj.get_local_name(),
|
||||
# "total_cars": make_counts.get(make_id, 0),
|
||||
# "models": {},
|
||||
# }
|
||||
|
||||
# # Process models
|
||||
# model_objects = {
|
||||
# m.id_car_model: m for m in
|
||||
# models.CarModel.objects.filter(id_car_model__in=_models)
|
||||
# .select_related('id_car_make')
|
||||
# .only('id_car_model', 'slug', 'name', 'arabic_name', 'id_car_make')
|
||||
# }
|
||||
|
||||
# for model_id in _models:
|
||||
# if not model_id:
|
||||
# continue
|
||||
|
||||
# model_obj = model_objects.get(model_id)
|
||||
# if not model_obj:
|
||||
# continue
|
||||
|
||||
# make_id = model_obj.id_car_make.id_car_make
|
||||
# if make_id not in inventory:
|
||||
# continue
|
||||
|
||||
# inventory[make_id]["models"][model_id] = {
|
||||
# "model_id": model_id,
|
||||
# "slug": model_obj.slug,
|
||||
# "model_name": model_obj.get_local_name(),
|
||||
# "total_cars": model_counts.get(model_id, 0),
|
||||
# "trims": {},
|
||||
# }
|
||||
|
||||
# # Process trims
|
||||
# trim_objects = {
|
||||
# t.id_car_trim: t for t in
|
||||
# models.CarTrim.objects.filter(id_car_trim__in=trims)
|
||||
# .select_related('id_car_serie__id_car_model__id_car_make')
|
||||
# .only('id_car_trim', 'slug', 'name', 'id_car_serie__id_car_model__id_car_make')
|
||||
# }
|
||||
|
||||
# for trim_id in trims:
|
||||
# if not trim_id:
|
||||
# continue
|
||||
|
||||
# trim_obj = trim_objects.get(trim_id)
|
||||
# if not trim_obj:
|
||||
# continue
|
||||
|
||||
# make_id = trim_obj.id_car_serie.id_car_model.id_car_make.id_car_make
|
||||
# model_id = trim_obj.id_car_serie.id_car_model.id_car_model
|
||||
|
||||
# if make_id not in inventory or model_id not in inventory[make_id]["models"]:
|
||||
# continue
|
||||
|
||||
# inventory[make_id]["models"][model_id]["trims"][trim_id] = {
|
||||
# "trim_id": trim_id,
|
||||
# "slug": trim_obj.slug,
|
||||
# "trim_name": trim_obj.name,
|
||||
# "total_cars": trim_counts.get(trim_id, 0),
|
||||
# }
|
||||
|
||||
# # Convert to final structure
|
||||
# result = {
|
||||
# "total_cars": total_cars,
|
||||
# "reserved_cars": reserved_cars,
|
||||
# "makes": [
|
||||
# {
|
||||
# "make_id": make_data["make_id"],
|
||||
# "slug": make_data["slug"],
|
||||
# "make_name": make_data["make_name"],
|
||||
# "total_cars": make_data["total_cars"],
|
||||
# "models": [
|
||||
# {
|
||||
# "model_id": model_data["model_id"],
|
||||
# "slug": model_data["slug"],
|
||||
# "model_name": model_data["model_name"],
|
||||
# "total_cars": model_data["total_cars"],
|
||||
# "trims": list(model_data["trims"].values()),
|
||||
# }
|
||||
# for model_data in make_data["models"].values()
|
||||
# if model_data["model_id"] # Skip empty models
|
||||
# ],
|
||||
# }
|
||||
# for make_data in inventory.values()
|
||||
# if make_data["make_id"] # Skip empty makes
|
||||
# ],
|
||||
# }
|
||||
###############################################
|
||||
# # Base queryset for cars belonging to the dealer
|
||||
cars = models.Car.objects.filter(dealer=request.dealer)
|
||||
|
||||
# Count for total, reserved, showroom, and unreserved cars
|
||||
@ -1435,6 +1620,18 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
context_object_name = "car"
|
||||
permission_required = ["inventory.view_car"]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = qs.select_related(
|
||||
"id_car_make",
|
||||
"id_car_model",
|
||||
"id_car_trim",
|
||||
"colors",
|
||||
"finances",
|
||||
"vendor",
|
||||
"registrations"
|
||||
)
|
||||
return qs
|
||||
|
||||
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
"""
|
||||
@ -5587,7 +5784,8 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
query = self.request.GET.get("q")
|
||||
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
|
||||
qs = models.Lead.objects.select_related("staff","id_car_make","id_car_model","customer").filter(dealer=dealer).exclude(status="converted")
|
||||
|
||||
if query:
|
||||
qs = qs.filter(Q(first_name__icontains=query)
|
||||
| Q(last_name__icontains=query)
|
||||
@ -5606,7 +5804,6 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
return models.Lead.objects.none()
|
||||
|
||||
|
||||
|
||||
class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""
|
||||
View that provides detailed information about a specific lead.
|
||||
@ -10107,11 +10304,28 @@ class BillModelActionMarkAsInReviewView(BaseBillActionView):
|
||||
class BillModelActionMarkAsApprovedView(BaseBillActionView):
|
||||
action_name = "mark_as_approved"
|
||||
|
||||
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
|
||||
if self.request.is_manager:
|
||||
messages.add_message(
|
||||
self.request,
|
||||
message="Bill updated successfully.",
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
return reverse("home",kwargs={"dealer_slug": dealer_slug})
|
||||
|
||||
return reverse(
|
||||
"bill-update",
|
||||
kwargs={
|
||||
"dealer_slug": dealer_slug,
|
||||
"entity_slug": entity_slug,
|
||||
"bill_pk": bill_pk,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class BillModelActionMarkAsPaidView(BaseBillActionView):
|
||||
action_name = "mark_as_paid"
|
||||
|
||||
|
||||
class BillModelActionDeleteView(BaseBillActionView):
|
||||
action_name = "mark_as_delete"
|
||||
|
||||
|
||||
@ -131,3 +131,4 @@ html[dir="rtl"] .form-icon-container .form-control {
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
@ -33,8 +33,6 @@
|
||||
<i class="fas fa-save me-2"></i>{% trans 'Save Bill' %}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
<a href="{% url 'bill_list' request.dealer.slug %}"
|
||||
@ -50,7 +48,6 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Bill Item Formset -->
|
||||
<div class="col-12">
|
||||
{% bill_item_formset_table itemtxs_formset %}
|
||||
|
||||
@ -221,12 +221,15 @@
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<!-- Update Button -->
|
||||
{% if perms.django_ledger.change_billmodel%}
|
||||
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary">
|
||||
<button class="btn btn-phoenix-primary" {% if not request.is_accountant %} disabled {% endif %}>
|
||||
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
</a>
|
||||
</button>
|
||||
<!-- Mark as Draft -->
|
||||
{% if bill.can_draft %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
{% if not request.is_accountant %} disabled {% endif %}
|
||||
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
@ -234,6 +237,7 @@
|
||||
<!-- Mark as Review -->
|
||||
{% if bill.can_review %}
|
||||
<button class="btn btn-phoenix-warning"
|
||||
{% if not request.is_accountant %} disabled {% endif %}
|
||||
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
||||
</button>
|
||||
@ -245,6 +249,11 @@
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bill.can_approve and not request.is_manager %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Paid -->
|
||||
{% if bill.can_pay %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
@ -262,6 +271,7 @@
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
{% if not request.is_accountant %} disabled {% endif %}
|
||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</button>
|
||||
|
||||
@ -137,6 +137,29 @@
|
||||
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-file-check-alt"></span>
|
||||
<h5 class="text-body-highlight fw-bold mb-0">{{ _("Lead Source")}}</h5>
|
||||
</div>
|
||||
{% if lead.source == 'REFERRALS' %}
|
||||
<span class="ms-2 fa fa-users"></span>
|
||||
{% elif lead.source == 'WHATSAPP' %}
|
||||
<span class="ms-2 fa fa-whatsapp"></span>
|
||||
{% elif lead.source == 'SHOWROOM' %}
|
||||
<span class="ms-2 fa fa-building"></span>
|
||||
{% elif lead.source == 'TIKTOK' %}
|
||||
<span class="ms-2 fa fa-tiktok"></span>
|
||||
{% elif lead.source == 'INSTAGRAM' %}
|
||||
<span class="ms-2 fa fa-instagram"></span>
|
||||
{% elif lead.source == 'X' %}
|
||||
<span class="ms-2 fa fa-times-circle"></span>
|
||||
{% elif lead.source == 'FACEBOOK' %}
|
||||
<span class="ms-2 fa fa-facebook-f"></span>
|
||||
{% elif lead.source == 'MOTORY' %}
|
||||
<span class="ms-2 fa fa-car-side"></span>
|
||||
{% elif lead.source == 'INFLUENCERS' %}
|
||||
<span class="ms-2 fa fa-user-check"></span>
|
||||
{% elif lead.source == 'YOUTUBE' %}
|
||||
<span class="ms-2 fa fa-youtube"></span>
|
||||
{% elif lead.source == 'CAMPAIGN' %}
|
||||
<span class="ms-2 fa fa-bullhorn"></span>
|
||||
{% endif %}
|
||||
<span class="text-body-secondary">{{ lead.source|upper }}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@ -490,6 +513,7 @@
|
||||
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="all-tasks-table-body">
|
||||
|
||||
@ -1,3 +1,19 @@
|
||||
<style>
|
||||
.fade-out {
|
||||
animation: fadeOut 1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<li class="nav-item dropdown">
|
||||
<!-- Notification counter -->
|
||||
<div class="notification-count">
|
||||
@ -236,6 +252,10 @@
|
||||
notificationCard.classList.remove('unread');
|
||||
notificationCard.classList.add('read');
|
||||
updateCounter('decrement');
|
||||
notificationCard.closest('.notification-card').classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
notificationCard.closest('.notification-card').remove();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
{% endif %}
|
||||
{% if po.po_status == 'fulfilled' %}
|
||||
{% if perms.inventory.add_car %}
|
||||
<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>
|
||||
<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 'Inventory Items' %}</a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
|
||||
@ -83,6 +83,11 @@
|
||||
{% if perms.django_ledger.can_approve_estimatemodel %}
|
||||
<button id="accept_estimate" onclick="setFormAction('approved')" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Mark As Approved' %}</span></button>
|
||||
{% endif %}
|
||||
{% if estimate.can_approve and not request.is_manager %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif estimate.status == 'approved' %}
|
||||
{% if perms.django_ledger.change_estimatemodel %}
|
||||
<a href="{% url 'send_email' request.dealer.slug estimate.pk %}" class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-sm-2"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a>
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if perms.django_ledger.add_payment%}
|
||||
{% if perms.inventory.add_payment%}
|
||||
{% if invoice.invoice_status == 'in_review' %}
|
||||
<button id="accept_invoice" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Accept' %}</span></button>
|
||||
{% endif %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user