diff --git a/inventory/models.py b/inventory/models.py index b12a8fe8..8c3ea81e 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -50,6 +50,9 @@ class DealerUserManager(UserManager): return user + + + class StaffUserManager(UserManager): def create_user_with_staff( self, @@ -946,9 +949,10 @@ class Staff(models.Model, LocalizedNameMixin): return self.staff_member.user @property - def groups(self): + def groups(self): return [x.customgroup for x in self.user.groups.all()] + def clear_groups(self): return self.user.groups.clear() diff --git a/inventory/views.py b/inventory/views.py index 3a926300..4da337d1 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,22 +1,77 @@ -from django.views.generic.edit import FormView -from django.db.models import Func -from django.contrib.auth.models import Permission -from appointment.models import Appointment,AppointmentRequest,Service,StaffMember -from calendar import month_name -from random import randint -from django_tables2 import SingleTableView -from django_tables2.export.views import ExportMixin -from plans.quota import get_user_quota +# Standard +import cv2 +import json +import logging +import numpy as np from rich import print +from random import randint from decimal import Decimal +from calendar import month_name +from pyzbar.pyzbar import decode +from urllib.parse import urlparse, urlunparse +##################################################################### + +# Django +from django.db.models import Q +from django.conf import settings +from django.db import transaction +from django.db.models import Func +from django.contrib import messages +from django.http import JsonResponse +from django.forms import HiddenInput +from django.shortcuts import HttpResponse +from django.db.models import Sum, F, Count from django.core.paginator import Paginator -from django.forms import DateField, DateInput, HiddenInput, TextInput +from django.contrib.auth.models import User +from django.contrib.auth.models import Group +from django.db.models import Count, F, Value +from django.urls import reverse, reverse_lazy +from django.utils import timezone, translation +from django.db.models.functions import Coalesce +from django.contrib.auth.models import Permission +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.core.files.storage import default_storage +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import DetailView, RedirectView +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.shortcuts import render, get_object_or_404, redirect +from django.views.generic import ( + View, + ListView, + DetailView, + CreateView, + UpdateView, + DeleteView, + TemplateView, +) +##################################################################### + +# Django Ledger +from django_ledger.io import roles +from django_ledger.utils import accruable_net_summary +from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm +from django_ledger.views.entity import EntityModelDetailBaseView,EntityModelDetailHandlerView +from django_ledger.forms.item import ( + ExpenseItemCreateForm, + ExpenseItemUpdateForm, +) +from django_ledger.forms.bank_account import ( + BankAccountCreateForm, + BankAccountUpdateForm, +) from django_ledger.forms.bill import ( ApprovedBillModelUpdateForm, InReviewBillModelUpdateForm, ) -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt +from django_ledger.forms.invoice import ( + DraftInvoiceModelUpdateForm, + ApprovedInvoiceModelUpdateForm, + PaidInvoiceModelUpdateForm, +) from django_ledger.models import ( ItemTransactionModel, EntityModel, @@ -27,68 +82,44 @@ from django_ledger.models import ( TransactionModel, EstimateModel, CustomerModel, - LedgerModel, ItemModel, BillModel, VendorModel, ) -from django_ledger.forms.bank_account import ( - BankAccountCreateForm, - BankAccountUpdateForm, +from django_ledger.views.financial_statement import ( + FiscalYearBalanceSheetView, + BaseIncomeStatementRedirectView, + FiscalYearIncomeStatementView, + BaseCashFlowStatementRedirectView, + FiscalYearCashFlowStatementView, ) -from django_ledger.forms.invoice import ( - DraftInvoiceModelUpdateForm, - ApprovedInvoiceModelUpdateForm, - PaidInvoiceModelUpdateForm, +from django_ledger.io.io_core import get_localdate +from django_ledger.models import EntityModel +from django_ledger.views.mixins import ( + QuarterlyReportMixIn, + MonthlyReportMixIn, + DateReportMixIn, + DjangoLedgerSecurityMixIn, + EntityUnitMixIn, ) -from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm -from django_ledger.forms.item import ( - ServiceCreateForm, - ExpenseItemCreateForm, - ExpenseItemUpdateForm, -) -import logging -import json -from django.db.models.functions import Coalesce, TruncDate, TruncDay -from django.shortcuts import HttpResponse -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.decorators import login_required -from django.http import JsonResponse -from django.shortcuts import render, get_object_or_404, redirect -from django.utils.translation import gettext_lazy as _ -from django.db.models import Q -from django.views.generic import ( - View, - ListView, - DetailView, - CreateView, - UpdateView, - DeleteView, - TemplateView, -) -from django.utils import timezone, translation -from django.conf import settings -from urllib.parse import urlparse, urlunparse -from django.urls import reverse, reverse_lazy -from django.contrib import messages -from django.db.models import Sum, F, Count -from django.db import transaction -from shapely.speedups import available -from django_ledger.io import roles +##################################################################### +# Other + +from plans.models import Plan +from . import models, forms, tables +from plans.quota import get_user_quota +from django_tables2 import SingleTableView +from django_tables2.export.views import ExportMixin +from appointment.models import Appointment,AppointmentRequest,Service,StaffMember + from .services import ( decodevin, get_make, get_model, ) -from . import models, forms, tables -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth.models import Group from .utils import ( CarFinanceCalculator, - calculate_vat_amount, - get_calculations, get_car_finance_data, get_financial_values, get_item_transactions, @@ -97,54 +128,16 @@ from .utils import ( get_user_type, set_bill_payment, set_invoice_payment, - to_dict, CarTransfer, ) -from django.contrib.auth.models import User -from allauth.account import views as allauth_views -from django.db.models import Count, F, Value -from django.contrib.auth import authenticate -import cv2 -import numpy as np -from pyzbar.pyzbar import decode -from django.core.files.storage import default_storage -from plans.models import Plan,PlanPricing -from django_ledger.utils import accruable_net_summary -from appointment.views_admin import fetch_user_appointments -from django_ledger.views.financial_statement import ( - FiscalYearBalanceSheetView, - BaseIncomeStatementRedirectView, - FiscalYearIncomeStatementView, - BaseCashFlowStatementRedirectView, - FiscalYearCashFlowStatementView, -) -from django_ledger.views.entity import EntityModelDetailBaseView,EntityModelDetailHandlerView -from django.views.generic import DetailView, RedirectView - -from django_ledger.io.io_core import get_localdate -from django_ledger.models import EntityModel, EntityUnitModel -from django_ledger.views.mixins import ( - QuarterlyReportMixIn, - YearlyReportMixIn, - MonthlyReportMixIn, - DateReportMixIn, - DjangoLedgerSecurityMixIn, - EntityUnitMixIn, - BaseDateNavigationUrlMixIn, - PDFReportMixIn, -) -from django_pdf_actions import actions - +##################################################################### logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) - - class Hash(Func): - function = 'get_hash' - + function = 'get_hash' def switch_language(request): language = request.GET.get("language", "en") @@ -322,7 +315,6 @@ class HomeView(TemplateView): class TestView(TemplateView): template_name = "inventory/cars_list_api.html" - class ManagerDashboard(LoginRequiredMixin, TemplateView): template_name = "dashboards/manager.html" @@ -880,14 +872,6 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView): ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) return form - # def get_initial(self): - # initial = super().get_initial() - # instance = self.get_object() - # dealer = get_user_type(self.request.user.dealer) - # selected_items = instance.additional_services.filter(dealer=dealer) - # initial["additional_finances"] = selected_items - # return initial - class CarFinanceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.CarFinance @@ -991,12 +975,6 @@ class CarTransferCreateView(CreateView): def get_success_url(self): return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) - -# def CarTransferDetailView(request, pk): -# transfer = get_object_or_404(models.CarTransfer, pk=pk) -# context = {"transfer": transfer} -# return render(request, "inventory/transfer_details.html", context) - class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView): model = models.CarTransfer template_name = "inventory/transfer_details.html" @@ -1055,7 +1033,6 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk): transfer.save() transfer_process = CarTransfer(car, transfer) success = transfer_process.transfer_car() - # success = CarTransfer(car, transfer) if success: messages.success(request, _("Car Transfer Completed successfully.")) models.Activity.objects.create(content_object=car,notes=f"Transfered from {transfer.from_dealer} to {transfer.to_dealer}",created_by=request.user) @@ -1071,46 +1048,6 @@ def CarTransferPreviewView(request, car_pk, transfer_pk): if transfer.to_dealer != get_user_type(request): return redirect("car_detail", pk=car_pk) return render(request, "inventory/transfer_preview.html", {"transfer": transfer}) - # def get_context_data(self, **kwargs): - # estimate = kwargs.get("object") - # if estimate.get_itemtxs_data(): - # data = get_financial_values(estimate) - - # kwargs["vat_amount"] = data["vat_amount"] - # kwargs["total"] = data["grand_total"] - # kwargs["discount_amount"] = data["discount_amount"] - # kwargs["vat"] = data["vat"] - # kwargs["car_and_item_info"] = data["car_and_item_info"] - # kwargs["additional_services"] = data["additional_services"] - # return super().get_context_data(**kwargs) - - -# class CarTransferView(View): -# template_name = "inventory/car_location_form.html" - -# def get(self, request, *args, **kwargs): -# form = forms.CarTransferForm() -# car = models.Car.objects.filter(pk=self.kwargs["pk"]) -# form.fields['to_dealer'].queryset = form.fields['to_dealer'].queryset.exclude(pk=get_user_type(request).pk) -# form.fields['car'].queryset = car -# form.initial['car'] = car.first() -# context = {"form": form} -# return render(request, self.template_name,context) - -# def post(self, request, *args, **kwargs): -# form = forms.CarTransferForm(request.POST) -# if form.is_valid(): -# from_dealer = get_user_type(request) -# car = form.cleaned_data['car'] -# to_dealer = form.cleaned_data['to_dealer'] -# remarks = form.cleaned_data['remarks'] -# models.CarTransferLog.objects.create(car=car, from_dealer=from_dealer, to_dealer=to_dealer, remarks=remarks) -# # car = models.Car.objects.filter(pk=self.kwargs["pk"]) -# # form.instance.car = car.first() -# # form.instance.to_dealer = get_user_type(request) -# # form.save() -# # messages.success(request, "Car transfered successfully.") -# return redirect("car_detail", pk=self.kwargs["pk"]) class CustomCardCreateView(LoginRequiredMixin, CreateView): @@ -1237,19 +1174,6 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): def get_success_url(self): return reverse("dealer_detail", kwargs={"pk": self.object.pk}) - # def get_form(self, form_class=None): - # form = super().get_form(form_class) - # if hasattr(form.fields, "dealer_type"): - # form.fields.pop("dealer_type") - # return form - # - # def get_form_class(self): - # if self.request.user.dealer.dealer_type == "OWNER": - # return forms.DealerForm - # else: - # return forms.UserForm - - class CustomerListView(LoginRequiredMixin, ListView): model = CustomerModel home_label = _("customers") @@ -1281,24 +1205,13 @@ class CustomerDetailView(LoginRequiredMixin, DetailView): dealer = get_user_type(self.request) entity = dealer.entity context = super().get_context_data(**kwargs) - # customer = f"{context['customer'].first_name} {context['customer'].middle_name} {context['customer'].last_name}" - # context["estimates"] = entity.get_estimates().filter( - # customer__customer_name=name - # ) + estimates = entity.get_estimates().filter(customer=self.object) invoices = entity.get_invoices().filter(customer=self.object) total = estimates.count() + invoices.count() context["estimates"] = estimates context["invoices"] = invoices - context["total"] = total - - - # context["notes"] = models.Notes.objects.filter( - # content_type__model="customer", object_id=self.object.id - # ) - # context["activities"] = models.Activity.objects.filter( - # content_type__model="customer", object_id=self.object.id - # ) + context["total"] = total return context @@ -1424,12 +1337,8 @@ def CustomerUpdateView(request, pk): if "customer_info" in customer.additional_info else {} ) - # form = CustomerModelForm return render(request, "customers/customer_form.html", {"form": form}) -# class CustomerUpdateView(UpdateView, LoginRequiredMixin, PermissionRequiredMixin): -# model = CustomerModel -# form_class = forms.CustomerForm -# template_name = "customers/customer_form.html" + @@ -1454,11 +1363,6 @@ class VendorListView(LoginRequiredMixin, ListView): vendors = dealer.entity.get_vendors().filter(active=True) return apply_search_filters(vendors, query) - - -# class VendorDetailView(LoginRequiredMixin, DetailView): -# model = models.Vendor -# template_name = "vendors/view_vendor.html" def vendorDetailView(request, pk): vendor = get_object_or_404(models.Vendor, pk=pk) return render(request, template_name="vendors/view_vendor.html", context={"vendor": vendor}) @@ -1478,8 +1382,6 @@ class VendorCreateView( def form_valid(self, form): dealer = get_user_type(self.request) form.instance.dealer = dealer - # instance = form.save(commit=False) - # instance.entity_model = dealer.entity form.instance.save() return super().form_valid(form) @@ -1504,383 +1406,6 @@ def delete_vendor(request, pk): messages.success(request, _("Vendor deleted successfully.")) return redirect("vendor_list") - -# class QuotationCreateView(LoginRequiredMixin, CreateView): -# model = models.SaleQuotation -# form_class = forms.QuotationForm -# template_name = "sales/quotation_form.html" -# -# def form_valid(self, form): -# form.instance.dealer = get_user_type(self.request) -# quotation = form.save() -# selected_cars = form.cleaned_data.get("cars") -# for car in selected_cars: -# car_finance = car.finances -# if car_finance: -# models.SaleQuotationCar.objects.create( -# quotation=quotation, -# car=car, -# ) -# -# messages.success(self.request, _("Quotation created successfully.")) -# return redirect("quotation_list") - - -# class QuotationListView(LoginRequiredMixin, ListView): -# model = models.SaleQuotation -# template_name = "sales/quotation_list.html" -# context_object_name = "quotations" -# paginate_by = 10 -# -# def get_queryset(self): -# status = self.request.GET.get("status") -# dealer = get_user_type(self.request) -# queryset = dealer.sales.all() -# if status: -# queryset = queryset.filter(status=status) -# return queryset - - -# class QuotationDetailView(LoginRequiredMixin, DetailView): -# model = models.SaleQuotation -# template_name = "sales/quotation_detail.html" -# context_object_name = "quotation" -# -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# quotation = self.object -# -# context_result = get_calculations(quotation) -# context.update(context_result) -# -# return context - - -# @login_required -# def generate_invoice(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# entity = dealer.entity -# if not quotation.is_approved: -# messages.error( -# request, "Quotation must be approved before converting to an invoice." -# ) -# else: -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# -# invoice_model = entity.create_invoice( -# customer_model=customer, -# terms=InvoiceModel.TERMS_ON_RECEIPT, -# cash_account=cash_account.first(), -# prepaid_account=recivable_account.first(), -# coa_model=coa_qs.first(), -# ) -# -# name_list = [ -# f"{instance.car.year} {instance.car.id_car_make} {instance.car.id_car_model} {instance.car.id_car_trim}" -# for instance in quotation.quotation_cars.all() -# ] -# -# invoices_item_models = invoice_model.get_item_model_qs().filter( -# name__in=name_list -# ) -# -# invoice_itemtxs = { -# im.item_number: { -# "unit_cost": im.default_amount, -# "quantity": 1, -# "total_amount": im.default_amount, -# } -# for im in invoices_item_models -# } -# -# invoice_itemtxs = invoice_model.migrate_itemtxs( -# itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND -# ) -# ledger = ( -# entity.get_ledgers() -# .filter(name=f"Payment Ledger for Invoice {invoice_model}") -# .first() -# ) -# if not ledger: -# ledger = entity.create_ledger( -# name=f"Payment Ledger for Invoice {invoice_model}", posted=True -# ) -# journal_entry = JournalEntryModel.objects.create( -# posted=False, -# description=f"Payment for Invoice {invoice_model}", -# ledger=ledger, -# locked=False, -# origin="Payment", -# ) -# -# quotation.payment_id = journal_entry.pk -# quotation.is_approved = True -# date = datetime.datetime.now() -# quotation.date_draft = date -# invoice_model.date_draft = date -# invoice_model.save() -# quotation.save() -# -# if not invoice_model.can_review(): -# messages.error(request, "Quotation is not ready for review") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_review() -# invoice_model.date_in_review = date -# quotation.date_in_review = date -# quotation.status = "In Review" -# invoice_model.save() -# quotation.save() - - # elif status == "approved": - # if qoutation.status == "Approved": - # messages.error(request, "Quotation is already approved") - # return redirect("quotation_detail", pk=pk) - - # invoice_model = invoice_model.filter(date_in_review=qoutation.date_in_review).first() - # if not invoice_model.can_approve(): - # messages.error(request, "Quotation is not ready for approval") - # return redirect("quotation_detail", pk=pk) - - # invoice_model.mark_as_approved(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) - # invoice_model.date_approved = date - # qoutation.date_approved = date - # invoice_model.save() - # qoutation.status = "Approved" - # qoutation.save() - # messages.success(request, _("Quotation Approved")) - # ledger = entity.create_ledger( - # name=f"Payment Ledger for Invoice {invoice_model}", - # posted=True - # ) - - # entity_unit,created = EntityUnitModel.objects.get_or_create( - # name="Sales Department", - # entity=entity, - # document_prefix="SD" - # ) - - # journal_entry = JournalEntryModel.objects.create( - # entity_unit=entity_unit, - # posted=False, - # description=f"Payment for Invoice {invoice_model}", - # ledger=ledger, - # locked=False, - # origin="Payment", - # ) - - # accounts_receivable = coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable").first() - # if not accounts_receivable: - # accounts_receivable = entity.create_account( - # code="AR", - # role="asset", - # name="Accounts Receivable", - # coa_model=coa_qs.first(), - # balance_type="credit" - # ) - - # TransactionModel.objects.create( - # journal_entry=journal_entry, - # account=cash_account.first(), # Debit Cash - # amount=invoice_model.amount_due, # Payment amount - # tx_type='debit', - # description="Payment Received", - # ) - - # TransactionModel.objects.create( - # journal_entry=journal_entry, - # account=accounts_receivable, # Credit Accounts Receivable - # amount=invoice_model.amount_due, # Payment amount - # tx_type='credit', - # description="Payment Received", - # ) - - # invoice_model.mark_as_review() - # print("reviewed") - # invoice_model.mark_as_approved(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) - # print("approved") - # invoice_model.mark_as_paid(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) - # print("paid") - # invoice_model.save() - # messages.success(request, "Invoice created") - # return redirect("quotation_detail", pk=pk) - - # return redirect('django_ledger:invoice-detail', entity_slug=quotation.entity.slug, invoice_pk=invoice.uuid) - - -# @login_required -# def post_quotation(request, pk): -# qoutation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# entity = dealer.entity -# if qoutation.posted: -# messages.error(request, "Quotation is already posted") -# return redirect("quotation_detail", pk=pk) -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# customer = ( -# entity.get_customers() -# .filter(customer_name=qoutation.customer.get_full_name) -# .first() -# ) -# invoice_model = ( -# entity.get_invoices() -# .filter(customer=customer, date_paid=qoutation.date_paid) -# .first() -# ) -# ledger = ( -# entity.get_ledgers() -# .filter(name=f"Payment Ledger for Invoice {invoice_model}") -# .first() -# ) -# return - # if not ledger: - # ledger = entity.create_ledger(name=f"Payment Ledger for Invoice {invoice_model}",posted=True) - - # entity_unit,created = EntityUnitModel.objects.get_or_create( - # name="Sales Department", - # entity=entity, - # document_prefix="SD" - # ) - - # journal_entry = JournalEntryModel.objects.create( - # entity_unit=entity_unit, - # posted=False, - # description=f"Payment for Invoice {invoice_model}", - # ledger=ledger, - # locked=False, - # origin="Payment", - # ) - - # TransactionModel.objects.create( - # journal_entry=journal_entry, - # account=cash_account.first(), # Debit Cash - # amount=invoice_model.amount_due, # Payment amount - # tx_type='debit', - # description="Payment Received", - # ) - - # TransactionModel.objects.create( - # journal_entry=journal_entry, - # account=recivable_account.first(), # Credit Accounts Receivable - # amount=invoice_model.amount_due, # Payment amount - # tx_type='credit', - # description="Payment Received", - # ) - # journal_entry.posted = True - # qoutation.posted = True - # qoutation.save() - # journal_entry.save() - # messages.success(request, "Invoice posted") - # return redirect("quotation_detail", pk=pk) - - -# @login_required -# def mark_quotation(request, pk): -# qoutation = get_object_or_404(models.SaleQuotation, pk=pk) -# status = request.GET.get("status") -# dealer = request.user.dealer -# entity = dealer.entity -# date = datetime.datetime.now() -# customer = ( -# entity.get_customers() -# .filter(customer_name=qoutation.customer.get_full_name) -# .first() -# ) -# invoice_model = entity.get_invoices().filter(customer=customer) -# if status == "approved": -# if qoutation.status == "Approved": -# messages.error(request, "Quotation is already approved") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model = invoice_model.filter( -# date_in_review=qoutation.date_in_review -# ).first() -# if not invoice_model.can_approve(): -# messages.error(request, "Quotation is not ready for approval") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_approved( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# invoice_model.date_approved = date -# qoutation.date_approved = date -# invoice_model.save() -# qoutation.status = "Approved" -# qoutation.save() -# for car in qoutation.quotation_cars.all(): -# car.car.status = "reserved" -# car.car.save() -# messages.success(request, _("Quotation Approved")) -# elif status == "paid": -# if qoutation.status == "Paid": -# messages.error(request, "Quotation is already paid") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model = invoice_model.filter( -# date_approved=qoutation.date_approved -# ).first() -# if not invoice_model.can_pay(): -# messages.error(request, "Quotation is not ready for payment") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_paid( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# invoice_model.date_paid = date -# qoutation.date_paid = date -# invoice_model.save() -# qoutation.status = "Paid" -# qoutation.save() -# messages.success(request, _("Quotation Paid")) -# return redirect("quotation_detail", pk=pk) - - -# @login_required -# def confirm_quotation(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# if quotation.is_approved: -# messages.error(request, _("Quotation already approved.")) -# return redirect("quotation_detail", pk=pk) -# -# try: -# # quotation.confirm() -# # quotation_cars = quotation.quotation_cars.annotate(total_price=F('car__total') * F('quantity')) -# # total = quotation.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) -# -# models.SalesOrder.objects.create( -# quotation=quotation, -# total_amount=quotation.total_vat, -# # total_amount=quotation.quotation_cars.aggregate(Sum("total_amount"))["total_amount__sum"], -# ) -# quotation.is_approved = True -# quotation.save() -# messages.success(request, _("Quotation confirmed and sales order created.")) -# except ValueError as e: -# messages.error(request, str(e)) -# return redirect("quotation_detail", pk=pk) - -# -# class SalesOrderDetailView(LoginRequiredMixin, DetailView): -# model = models.SalesOrder -# template_name = "sales/sales_order_detail.html" -# context_object_name = "sales_order" -# slug_field = "order_id" -# slug_url_kwarg = "order_id" - #group class GroupListView(LoginRequiredMixin, ListView): model = models.CustomGroup @@ -1936,36 +1461,6 @@ class GroupUpdateView( instance.group.name = f"{dealer.pk}_{instance.name}" instance.save() return super().form_valid(form) - - # def get_form_kwargs(self): - # kwargs = super().get_form_kwargs() - # kwargs["instance"] = self.get_object() # Pass the Staff instance to the form - # return kwargs - - # def get_form(self, form_class = None): - # form = super().get_form(form_class) - # form.fields['email'].disabled = True - # return form - # def get_initial(self): - # initial = super().get_initial() - # initial['service_offered'] = self.object.staff_member.services_offered.all() - # initial['email'] = self.object.staff_member.user.email - # 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) - - # 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.save() - # return super().form_valid(form) def GroupDeleteview(request, pk): group = get_object_or_404(models.CustomGroup, pk=pk) @@ -1990,6 +1485,7 @@ def GroupPermissionView(request, pk): def UserGroupView(request, pk): staff = get_object_or_404(models.Staff, pk=pk) + if request.method == "POST": form = forms.UserGroupForm(request.POST) groups = request.POST.getlist("name") @@ -2002,6 +1498,7 @@ def UserGroupView(request, pk): return redirect("user_detail", pk=staff.pk) form = forms.UserGroupForm(initial={"name": staff.groups}) + form.fields['name'].queryset = models.CustomGroup.objects.filter(dealer=staff.dealer) return render(request,"users/user_group_form.html",{"staff": staff, "form": form}) class UserListView(LoginRequiredMixin, ListView): @@ -2146,8 +1643,6 @@ class OrganizationListView(LoginRequiredMixin, ListView): return apply_search_filters(organization, query) - - class OrganizationDetailView(DetailView): model = CustomerModel template_name = "organizations/organization_detail.html" @@ -2279,183 +1774,6 @@ class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVi success_message = "Representative deleted successfully." -# def quotation_pdf_view(request, pk): -# # Get the quotation object -# quotation = models.SaleQuotation.objects.get(pk=pk) -# -# # Render the HTML template for the quotation page -# context = { -# "quotation": quotation, -# } -# context_result = get_calculations(quotation) -# context = context.update(context_result) -# -# html_content = render_to_string("sales/quotation_pdf.html", context) -# -# # Create a PDF file -# -# pdf_file = HTML(string=html_content).render() -# -# # Save the PDF file to a file -# with open("quotation.pdf", "wb") as f: -# f.write(pdf_file.write_pdf()) -# -# # Return the PDF file as a response -# return HttpResponse(pdf_file, content_type="application/pdf") - - -# @login_required -# def download_quotation_pdf(request, quotation_id): -# try: -# # Retrieve the quotation object -# quotation = models.SaleQuotation.objects.get(id=quotation_id) -# cars = models.SaleQuotationCar.objects.get(id=quotation_id) -# print(cars) -# services = cars.finance.additional_services.all() -# print(services) -# -# # Create a response object -# response = HttpResponse(content_type="application/pdf") -# response["Content-Disposition"] = ( -# f'attachment; filename="quotation_{quotation.id}.pdf"' -# ) -# -# # Call the PDF generation function -# # generate_quotation_pdf(response, quotation, services) -# -# return response -# except models.SaleQuotation.DoesNotExist: -# return HttpResponse("Quotation not found", status=404) - - -# @login_required -# def invoice_detail(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = request.user.dealer -# entity = dealer.entity -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# invoice_model = entity.get_invoices() -# -# invoice = invoice_model.filter( -# customer=customer, date_draft=quotation.date_draft -# ).first() -# return redirect("quotation_detail", pk=pk) - - -# @login_required -# def payment_invoice(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = request.user.dealer -# entity = dealer.entity -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# invoice_model = entity.get_invoices() -# invoice = invoice_model.filter( -# customer=customer, date_draft=quotation.date_draft -# ).first() -# -# return redirect("quotation_detail", pk=pk) - - -# class PaymentCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): -# model = models.Payment -# form_class = forms.PaymentForm -# template_name = "sales/payments/payment_form.html" -# success_url = reverse_lazy("quotation_list") -# success_message = "Payment created successfully." - -# def form_valid(self, form): -# quotation = get_object_or_404(models.SaleQuotation, pk=self.kwargs["pk"]) -# form.instance.quotation = quotation -# form.save() -# return super().form_valid(form) -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context["quotation"] = get_object_or_404(models.SaleQuotation, pk=self.kwargs["pk"]) -# return context - - -# def payment_create(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# if request.method == "POST": -# form = forms.PaymentForm(request.POST) -# if form.is_valid(): -# form.instance.quotation = quotation -# insatnce = form.save() -# -# dealer = dealer -# entity = dealer.entity -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# journal_entry = JournalEntryModel.objects.filter( -# pk=quotation.payment_id -# ).first() -# TransactionModel.objects.create( -# journal_entry=journal_entry, -# account=cash_account.first(), # Debit Cash -# amount=insatnce.amount, # Payment amount -# tx_type="debit", -# description="Payment Received", -# ) -# -# TransactionModel.objects.create( -# journal_entry=journal_entry, -# account=recivable_account.first(), # Credit Accounts Receivable -# amount=insatnce.amount, # Payment amount -# tx_type="credit", -# description="Payment Received", -# ) -# journal_entry.posted = True -# quotation.posted = True -# quotation.save() -# journal_entry.save() -# -# invoice_model = ( -# entity.get_invoices() -# .filter(date_approved=quotation.date_approved) -# .first() -# ) -# -# invoice_model.mark_as_paid( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# date = timezone.now() -# invoice_model.date_paid = date -# quotation.date_paid = date -# invoice_model.save() -# quotation.status = "Paid" -# quotation.save() -# -# messages.success(request, "Payment created successfully.") -# return redirect("quotation_detail", pk=pk) -# else: -# form = forms.PaymentForm() -# return render( -# request, -# "sales/payments/payment_create.html", -# {"quotation": quotation, "form": form}, -# ) - - -# Ledger - - # BANK ACCOUNT class BankAccountListView(LoginRequiredMixin, ListView): model = BankAccountModel @@ -2526,10 +1844,8 @@ def bank_account_delete(request, pk): {"bank_account": bank_account}, ) - # Accounts - class AccountListView(LoginRequiredMixin, ListView): model = AccountModel template_name = "ledger/coa_accounts/account_list.html" @@ -2608,7 +1924,6 @@ class AccountDetailView(LoginRequiredMixin, DetailView): return context - class AccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = AccountModel form_class = AccountModelUpdateForm @@ -2662,41 +1977,6 @@ class EstimateListView(LoginRequiredMixin, ListView): queryset = queryset.filter(status=status) return queryset - - - - - -# class EstimateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): -# model = EstimateModel -# form_class = EstimateModelCreateForm -# template_name = "sales/estimates/estimate_form.html" -# success_url = reverse_lazy("estimate_list") -# success_message = "Estimate created successfully." - -# def get_form_kwargs(self): -# """ -# Override this method to pass additional keyword arguments to the form. -# """ -# entity = self.request.user.dealer.entity -# kwargs = super().get_form_kwargs() -# kwargs['entity_slug'] = entity.slug -# kwargs['user_model'] = entity.admin -# return kwargs - -# def get_context_data(self, **kwargs): -# entity = self.request.user.dealer.entity -# kwargs['items'] = entity.get_items_all() -# return super().get_context_data(**kwargs) -# def get_customer_queryset(self): -# entity = self.request.user.dealer.entity -# return entity.get_customer_queryset() - -# def form_valid(self, form): -# form.instance.entity = self.request.user.dealer.entity -# return super().form_valid(form) - - # @csrf_exempt @login_required def create_estimate(request,pk=None): @@ -2760,10 +2040,8 @@ def create_estimate(request,pk=None): ] items_txs = [] for item in items_list: - # item_instance = ItemModel.objects.get(pk=item.get("item_id")) car_instance = ItemModel.objects.filter(additional_info__car_info__hash=item.get("item_id")).all() - # car_instance = models.Car.objects.get(vin=item_instance.name) for i in car_instance[:int(quantities[0])]: items_txs.append( { @@ -2806,11 +2084,7 @@ def create_estimate(request,pk=None): for item in estimate_itemtxs.keys(): item_instance = ItemModel.objects.filter(item_number=item).first() instance = models.Car.objects.get(vin=item_instance.name) - reserve_car(instance, request) - # for item in items: - # item_instance = ItemModel.objects.filter(additioinal_info__car_info__hash=item).first() - # instance = models.Car.objects.get(hash=item) - # reserve_car(instance, request) + reserve_car(instance, request) else: item_instance = ItemModel.objects.filter(additioinal_info__car_info__hash=items).first() @@ -2908,7 +2182,6 @@ def create_sale_order(request, pk): form = forms.SaleOrderForm() form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) form.initial["estimate"] = estimate - # data = get_car_finance_data(estimate) calculator = CarFinanceCalculator(estimate) finance_data = calculator.get_finance_data() return render( @@ -2957,7 +2230,6 @@ class EstimatePreviewView(DetailView): kwargs["total"] = data["grand_total"] kwargs["discount_amount"] = data["discount_amount"] kwargs["vat"] = data["vat"] - # kwargs["car_and_item_info"] = data["car_and_item_info"] kwargs["additional_services"] = data["additional_services"] return super().get_context_data(**kwargs) @@ -3016,7 +2288,6 @@ class InvoiceListView(LoginRequiredMixin, ListView): invoices = dealer.entity.get_invoices() return apply_search_filters(invoices, query) - class InvoiceDetailView(LoginRequiredMixin, DetailView): model = InvoiceModel template_name = "sales/invoices/invoice_detail.html" @@ -3128,34 +2399,10 @@ def invoice_create(request, pk): ledger.save() invoice.save() - # unit_items = estimate.get_itemtxs_data()[0] - # vat = models.VatRate.objects.filter(is_active=True).first() + calculator = CarFinanceCalculator(estimate) finance_data = calculator.get_finance_data() - - # total = 0 - # discount_amount = 0 - - # itemtxs = [] - # for item in unit_items: - # car = models.Car.objects.get(vin=item.item_model.name) - - # total = Decimal(car.finances.total) * Decimal(item.ce_quantity) - # discount_amount = car.finances.discount_amount - - # grand_total = Decimal(total) - Decimal(discount_amount) - # vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) - # grand_total += Decimal(vat_amount) - # unit_cost = grand_total / Decimal(item.ce_quantity) - # itemtxs.append( - # { - # "item_number": item.item_model.item_number, - # "unit_cost": unit_cost, - # "unit_revenue": unit_cost, - # "quantity": item.ce_quantity, - # "total_amount": grand_total, - # } - # ) + invoice_itemtxs = { i.get("item_number"): { "unit_cost": i.get("total_vat"), @@ -3206,23 +2453,14 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView): dealer = get_user_type(self.request) invoice = kwargs.get("object") if invoice.get_itemtxs_data(): - # data = get_financial_values(invoice) calculator = CarFinanceCalculator(invoice) finance_data = calculator.get_finance_data() kwargs["data"] = finance_data kwargs['dealer'] = dealer - # kwargs["vat_amount"] = data["vat_amount"] - # kwargs["total"] = data["grand_total"] - # kwargs["discount_amount"] = data["discount_amount"] - # kwargs["vat"] = data["vat"] - # kwargs["car_and_item_info"] = data["car_and_item_info"] - # kwargs["additional_services"] = data["additional_services"] return super().get_context_data(**kwargs) - # payments - def PaymentCreateView(request, pk): invoice = InvoiceModel.objects.filter(pk=pk).first() bill = BillModel.objects.filter(pk=pk).first() @@ -3260,7 +2498,6 @@ def PaymentCreateView(request, pk): messages.error(request, f"Error creating payment: {str(e)}") else: messages.error(request, f"Invalid form data: {str(form.errors)}") - # return redirect(redirect_url, pk=model.pk) form = forms.PaymentForm() if model: form.initial["amount"] = model.amount_due - model.amount_paid @@ -3277,7 +2514,6 @@ def PaymentCreateView(request, pk): def PaymentListView(request): dealer = get_user_type(request) - entity = dealer.entity journals = JournalEntryModel.objects.filter(ledger__entity=entity).all() return render(request, "sales/payments/payment_list.html", {"journals": journals}) @@ -3673,10 +2909,6 @@ class OpportunityCreateView(CreateView, LoginRequiredMixin): def form_valid(self, form): dealer = get_user_type(self.request) form.instance.dealer = dealer - # staff = dealer.staff - print(dealer) - # print(staff) - # form.instance.staff = staff return super().form_valid(form) def get_success_url(self): @@ -3743,27 +2975,9 @@ def opportunity_update_status(request,pk): opportunity.stage = stage opportunity.save() messages.success(request,"Opportunity status updated successfully") - # response = HttpResponse(render(request, "crm/opportunities/opportunity_detail.html"),{"opportunity":opportunity}) response = HttpResponse(redirect("opportunity_detail",pk=opportunity.pk)) response['HX-Refresh'] = 'true' return response - # return render(request,"crm/opportunities/opportunity_detail.html",{"opportunity":opportunity}) - -# class OpportunityLogsView(LoginRequiredMixin, ListView): -# model = models.OpportunityLog -# template_name = "crm/opportunity_logs.html" -# context_object_name = "logs" -# -# def get_queryset(self): -# opportunity_id = self.kwargs["pk"] -# return models.OpportunityLog.objects.filter( -# opportunity_id=opportunity_id -# ).order_by("-created_at") -# -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context["opportunity"] = models.Opportunity.objects.get(pk=self.kwargs["pk"]) -# return context class NotificationListView(LoginRequiredMixin, ListView): @@ -3791,15 +3005,7 @@ def fetch_notifications(request): notifications = models.Notification.objects.filter( user=request.user, is_read=False ).order_by("-created") - # notifications_data = [ - # { - # "id": notification.id, - # "message": notification.message, - # "created": notification.created.strftime("%Y-%m-%d %H:%M:%S"), - # } - # for notification in notifications - # ] - # return JsonResponse({"notifications": notifications_data}) + return render(request, "notifications.html", {"notifications_": notifications}) @@ -3929,9 +3135,7 @@ class BillDetailView(LoginRequiredMixin, DetailView): Decimal(x.unit_cost) * Decimal(x.quantity) for x in txs ) - # vat = models.VatRate.objects.filter(is_active=True).first() - # if vat: - # grand_total += round(Decimal(grand_total) * Decimal(vat.rate), 2) + kwargs["transactions"] = transactions kwargs["grand_total"] = grand_total print(dir(txs[0])) @@ -4023,37 +3227,6 @@ def bill_mark_as_paid(request, pk): messages.error(request, _("Amount paid is not equal to amount due.")) return redirect("bill_detail", pk=bill.pk) - # def get_context_data(self, **kwargs): - # dealer = get_user_type(self.request) - # context = super().get_context_data(**kwargs) - # context['entity_model'] = dealer.entity - # context['user_model'] = dealer.entity.admin - - # return context - - -# class BillCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): -# model = BillModel -# form_class = BillModelCreateForm -# template_name = "ledger/bills/bill_form.html" -# success_url = reverse_lazy("bill_list") -# success_message = _("Bill created successfully.") -# def get_form_kwargs(self): -# dealer = get_user_type(self.request) -# kwargs = super().get_form_kwargs() -# kwargs["entity_model"] = dealer.entity -# return kwargs - - -# def form_valid(self, form): -# dealer = get_user_type(self.request) -# form.instance.entity = dealer.entity -# ledger = dealer.entity.create_ledger( -# name=f"Bill for Vendor {form.instance.vendor.vendor_name}", posted=True -# ) -# form.instance.ledger = ledger -# return super().form_valid(form) - @login_required def bill_create(request): dealer = get_user_type(request) @@ -4176,13 +3349,6 @@ def BillDeleteView(request, pk): bill.delete() return redirect("bill_list") - -# class SubscriptionPlans(ListView, LoginRequiredMixin): -# model = models.SubscriptionPlan -# template_name = "subscriptions/subscription_plan.html" -# context_object_name = "plans" - - # orders class OrderListView(ListView): model = models.SaleOrder @@ -4195,9 +3361,6 @@ class OrderListView(ListView): def send_email_view(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) if request.method == "POST": - # if not estimate.can_review(): - # messages.error(request, "Estimate is not ready for review") - # return redirect("estimate_detail", pk=estimate.pk) if not estimate.get_itemtxs_data()[0]: messages.error(request, _("Estimate has no items")) return redirect("estimate_detail", pk=estimate.pk) @@ -4456,11 +3619,6 @@ class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): user_model=dealer.entity.admin, ).unpaid() - # todo: implement this... - # unit_slug = self.get_unit_slug() - # if unit_slug: - # bill_qs.filter(ledger__journal_entry__entity_unit__slug__exact=unit_slug) - net_summary = accruable_net_summary(bill_qs) net_payables = { 'net_payable_data': net_summary @@ -4486,11 +3644,6 @@ class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View): user_model=dealer.entity.admin, ).unpaid() - # todo: implement this... - # unit_slug = self.get_unit_slug() - # if unit_slug: - # invoice_qs.filter(ledger__journal_entry__entity_unit__slug__exact=unit_slug) - net_summary = accruable_net_summary(invoice_qs) net_receivable = { @@ -4595,25 +3748,6 @@ class CarListViewTable(ExportMixin, LoginRequiredMixin, SingleTableView): "finances", "colors__exterior", "colors__interior" ).filter(dealer=dealer) - -# class UserSettingsView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): -# template_name = 'account/user_settings.html' -# form_class = forms.UserSettingsForm -# model = models.UserSettings -# success_message = 'User settings updated' - -# def get_object(self, queryset=None): -# return models.UserSettings.objects.get(user=self.request.user) - -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context['form'] = self.form_class(instance=self.get_object()) -# return context - -# def form_valid(self, form): -# form.instance.user = self.request.user -# return super().form_valid(form) - def DealerSettingsView(request,pk): dealer_setting = get_object_or_404(models.DealerSettings, pk=pk) dealer = get_user_type(request) diff --git a/static/css/nice-select2.css b/static/css/nice-select2.css new file mode 100644 index 00000000..f77d1a01 --- /dev/null +++ b/static/css/nice-select2.css @@ -0,0 +1 @@ +.nice-select{-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:#fff;border-radius:5px;border:solid 1px #e8e8e8;box-sizing:border-box;clear:both;cursor:pointer;display:block;float:left;font-family:inherit;font-size:14px;font-weight:normal;height:38px;line-height:36px;outline:none;padding-left:18px;padding-right:30px;position:relative;text-align:left !important;transition:all .2s ease-in-out;user-select:none;white-space:nowrap;width:auto}.nice-select:hover{border-color:hsl(0,0%,85.9803921569%)}.nice-select:active,.nice-select.open,.nice-select:focus{border-color:#999}.nice-select:after{border-bottom:2px solid #999;border-right:2px solid #999;content:"";display:block;height:5px;margin-top:-4px;pointer-events:none;position:absolute;right:12px;top:50%;transform-origin:66% 66%;transform:rotate(45deg);transition:all .15s ease-in-out;width:5px}.nice-select.open:after{transform:rotate(-135deg)}.nice-select.open .nice-select-dropdown{opacity:1;pointer-events:auto;transform:scale(1) translateY(0)}.nice-select.disabled{border-color:rgb(237.1,237.1,237.1);color:#999;pointer-events:none}.nice-select.disabled:after{border-color:#ccc}.nice-select.wide{width:100%}.nice-select.wide .nice-select-dropdown{left:0 !important;right:0 !important}.nice-select.right{float:right}.nice-select.right .nice-select-dropdown{left:auto;right:0}.nice-select.small{font-size:12px;height:36px;line-height:34px}.nice-select.small:after{height:4px;width:4px}.nice-select.small .option{line-height:34px;min-height:34px}.nice-select .nice-select-dropdown{margin-top:4px;background-color:#fff;border-radius:5px;box-shadow:0 0 0 1px rgba(68,68,68,.11);pointer-events:none;position:absolute;top:100%;left:0;transform-origin:50% 0;transform:scale(0.75) translateY(19px);transition:all .2s cubic-bezier(0.5, 0, 0, 1.25),opacity .15s ease-out;z-index:9;opacity:0}.nice-select .list{border-radius:5px;box-sizing:border-box;overflow:hidden;padding:0;max-height:210px;overflow-y:auto}.nice-select .list:hover .option:not(:hover){background-color:rgba(0,0,0,0) !important}.nice-select .option{cursor:pointer;font-weight:400;line-height:40px;list-style:none;outline:none;padding-left:18px;padding-right:29px;text-align:left;transition:all .2s}.nice-select .option:hover,.nice-select .option.focus,.nice-select .option.selected.focus{background-color:#f6f6f6}.nice-select .option.selected{font-weight:bold}.nice-select .option.disabled{background-color:rgba(0,0,0,0);color:#999;cursor:default}.nice-select .extra{float:right}.nice-select .optgroup{font-weight:bold}.no-csspointerevents .nice-select .nice-select-dropdown{display:none}.no-csspointerevents .nice-select.open .nice-select-dropdown{display:block}.nice-select .list::-webkit-scrollbar{width:0}.nice-select .has-multiple{white-space:inherit;height:auto;padding:7px 12px;min-height:36px;line-height:22px}.nice-select .has-multiple span.current{border:1px solid #ccc;background:#eee;padding:0 10px;border-radius:3px;display:inline-block;line-height:24px;font-size:14px;margin-bottom:3px;margin-right:3px}.nice-select .has-multiple .multiple-options{display:block;line-height:24px;padding:0}.nice-select .nice-select-search-box{box-sizing:border-box;width:100%;padding:5px;pointer-events:none;border-radius:5px 5px 0 0}.nice-select .nice-select-search{box-sizing:border-box;background-color:#fff;border:1px solid #e8e8e8;border-radius:3px;color:#444;display:inline-block;vertical-align:middle;padding:7px 12px;margin:0 10px 0 0;width:100%;min-height:36px;line-height:22px;height:auto;outline:0 !important;font-size:14px} diff --git a/static/js/nice-select2.js b/static/js/nice-select2.js new file mode 100644 index 00000000..ba0393d5 --- /dev/null +++ b/static/js/nice-select2.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.NiceSelect=t():e.NiceSelect=t()}(self,(()=>(()=>{"use strict";var e={d:(t,i)=>{for(var s in i)e.o(i,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:i[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function i(e){const t=new MouseEvent("click",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}function s(e){const t=new Event("change",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}function o(e){const t=new FocusEvent("focusin",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}function n(e){const t=new FocusEvent("focusout",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}function l(e){const t=new UIEvent("modalclose",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}function d(e,t){"invalid"==t?(c(this.dropdown,"invalid"),p(this.dropdown,"valid")):(c(this.dropdown,"valid"),p(this.dropdown,"invalid"))}function r(e,t){return null!=e[t]?e[t]:e.getAttribute(t)}function a(e,t){return!!e&&e.classList.contains(t)}function c(e,t){if(e)return e.classList.add(t)}function p(e,t){if(e)return e.classList.remove(t)}e.r(t),e.d(t,{bind:()=>f,default:()=>u});var h={data:null,searchable:!1,showSelectedItems:!1};function u(e,t){this.el=e,this.config=Object.assign({},h,t||{}),this.data=this.config.data,this.selectedOptions=[],this.placeholder=r(this.el,"placeholder")||this.config.placeholder||"Select an option",this.searchtext=r(this.el,"searchtext")||this.config.searchtext||"Search",this.selectedtext=r(this.el,"selectedtext")||this.config.selectedtext||"selected",this.dropdown=null,this.multiple=r(this.el,"multiple"),this.disabled=r(this.el,"disabled"),this.create()}function f(e,t){return new u(e,t)}return u.prototype.create=function(){this.el.style.opacity="0",this.el.style.width="0",this.el.style.padding="0",this.el.style.height="0",this.el.style.fontSize="0",this.data?this.processData(this.data):this.extractData(),this.renderDropdown(),this.bindEvent()},u.prototype.processData=function(e){var t=[];e.forEach((e=>{t.push({data:e,attributes:{selected:!!e.selected,disabled:!!e.disabled,optgroup:"optgroup"==e.value}})})),this.options=t},u.prototype.extractData=function(){var e=this.el.querySelectorAll("option,optgroup"),t=[],i=[],s=[];e.forEach((e=>{if("OPTGROUP"==e.tagName)var s={text:e.label,value:"optgroup"};else{let t=e.innerText;null!=e.dataset.display&&(t=e.dataset.display),s={text:t,value:e.value,extra:e.dataset.extra,selected:null!=e.getAttribute("selected"),disabled:null!=e.getAttribute("disabled")}}var o={selected:null!=e.getAttribute("selected"),disabled:null!=e.getAttribute("disabled"),optgroup:"OPTGROUP"==e.tagName};t.push(s),i.push({data:s,attributes:o})})),this.data=t,this.options=i,this.options.forEach((e=>{e.attributes.selected&&s.push(e)})),this.selectedOptions=s},u.prototype.renderDropdown=function(){var e=["nice-select",r(this.el,"class")||"",this.disabled?"disabled":"",this.multiple?"has-multiple":""];let t='";var i=`
`;i+=``,i+='
',i+=`${this.config.searchable?t:""}`,i+='',i+="
",i+="
",this.el.insertAdjacentHTML("afterend",i),this.dropdown=this.el.nextElementSibling,this._renderSelectedItems(),this._renderItems()},u.prototype._renderSelectedItems=function(){if(this.multiple){var e="";this.config.showSelectedItems||this.config.showSelectedItems||"auto"==window.getComputedStyle(this.dropdown).width||this.selectedOptions.length<2?(this.selectedOptions.forEach((function(t){e+=`${t.data.text}`})),e=""==e?this.placeholder:e):e=this.selectedOptions.length+" "+this.selectedtext,this.dropdown.querySelector(".multiple-options").innerHTML=e}else{var t=this.selectedOptions.length>0?this.selectedOptions[0].data.text:this.placeholder;this.dropdown.querySelector(".current").innerHTML=t}},u.prototype._renderItems=function(){var e=this.dropdown.querySelector("ul");this.options.forEach((t=>{e.appendChild(this._renderItem(t))}))},u.prototype._renderItem=function(e){var t=document.createElement("li");if(t.innerHTML=e.data.text,null!=e.data.extra&&t.appendChild(this._renderItemExtra(e.data.extra)),e.attributes.optgroup)c(t,"optgroup");else{t.setAttribute("data-value",e.data.value);var i=["option",e.attributes.selected?"selected":null,e.attributes.disabled?"disabled":null];t.addEventListener("click",this._onItemClicked.bind(this,e)),t.classList.add(...i)}return e.element=t,t},u.prototype._renderItemExtra=function(e){var t=document.createElement("span");return t.innerHTML=e,c(t,"extra"),t},u.prototype.update=function(){if(this.extractData(),this.dropdown){var e=a(this.dropdown,"open");this.dropdown.parentNode.removeChild(this.dropdown),this.create(),e&&i(this.dropdown)}r(this.el,"disabled")?this.disable():this.enable()},u.prototype.disable=function(){this.disabled||(this.disabled=!0,c(this.dropdown,"disabled"))},u.prototype.enable=function(){this.disabled&&(this.disabled=!1,p(this.dropdown,"disabled"))},u.prototype.clear=function(){this.resetSelectValue(),this.selectedOptions=[],this._renderSelectedItems(),this.update(),s(this.el)},u.prototype.destroy=function(){this.dropdown&&(this.dropdown.parentNode.removeChild(this.dropdown),this.el.style.display="")},u.prototype.bindEvent=function(){this.dropdown.addEventListener("click",this._onClicked.bind(this)),this.dropdown.addEventListener("keydown",this._onKeyPressed.bind(this)),this.dropdown.addEventListener("focusin",o.bind(this,this.el)),this.dropdown.addEventListener("focusout",n.bind(this,this.el)),this.el.addEventListener("invalid",d.bind(this,this.el,"invalid")),window.addEventListener("click",this._onClickedOutside.bind(this)),this.config.searchable&&this._bindSearchEvent()},u.prototype._bindSearchEvent=function(){var e=this.dropdown.querySelector(".nice-select-search");e&&e.addEventListener("click",(function(e){return e.stopPropagation(),!1})),e.addEventListener("input",this._onSearchChanged.bind(this))},u.prototype._onClicked=function(e){if(e.preventDefault(),a(this.dropdown,"open")?this.multiple?e.target==this.dropdown.querySelector(".multiple-options")&&(p(this.dropdown,"open"),l(this.el)):(p(this.dropdown,"open"),l(this.el)):(c(this.dropdown,"open"),function(e){const t=new UIEvent("modalopen",{bubbles:!0,cancelable:!1});e.dispatchEvent(t)}(this.el)),a(this.dropdown,"open")){var t=this.dropdown.querySelector(".nice-select-search");t&&(t.value="",t.focus());var i=this.dropdown.querySelector(".focus");p(i,"focus"),c(i=this.dropdown.querySelector(".selected"),"focus"),this.dropdown.querySelectorAll("ul li").forEach((function(e){e.style.display=""}))}else this.dropdown.focus()},u.prototype._onItemClicked=function(e,t){var i=t.target;if(!a(i,"disabled")){if(this.multiple)if(a(i,"selected")){p(i,"selected"),this.selectedOptions.splice(this.selectedOptions.indexOf(e),1);var s=this.el.querySelector(`option[value="${i.dataset.value}"]`);s.removeAttribute("selected"),s.selected=!1}else c(i,"selected"),this.selectedOptions.push(e);else this.options.forEach((function(e){p(e.element,"selected")})),this.selectedOptions.forEach((function(e){p(e.element,"selected")})),c(i,"selected"),this.selectedOptions=[e];this._renderSelectedItems(),this.updateSelectValue()}},u.prototype.setValue=function(e){var t,i=this.el,s=!0;if(i.multiple)for(var o=0;o-1?n.value:null:e,n.value!=t||n.disabled?(n.removeAttribute("selected"),delete n.selected):(s&&(i.value=t,s=!1),n.setAttribute("selected",!0),n.selected=!0);s&&!i.multiple&&(i.options[0].setAttribute("selected",!0),i.options[0].selected=!0,i.value=i.options[0].value),this.update()},u.prototype.getValue=function(){var e=this.el;if(!e.multiple)return e.value;var t=[];for(var i of e.options)i.selected&&t.push(i.value);return t},u.prototype.updateSelectValue=function(){if(this.multiple){var e=this.el;this.selectedOptions.forEach((function(t){var i=e.querySelector(`option[value="${t.data.value}"]`);i?i.setAttribute("selected",!0):console.error("Option not found, does it have a value?")}))}else this.selectedOptions.length>0&&(this.el.value=this.selectedOptions[0].data.value);s(this.el)},u.prototype.resetSelectValue=function(){if(this.multiple){var e=this.el;this.selectedOptions.forEach((function(t){var i=e.querySelector(`option[value="${t.data.value}"]`);i&&(i.removeAttribute("selected"),delete i.selected)}))}else this.selectedOptions.length>0&&(this.el.selectedIndex=-1);s(this.el)},u.prototype._onClickedOutside=function(e){this.dropdown.contains(e.target)||(p(this.dropdown,"open"),l(this.el))},u.prototype._onKeyPressed=function(e){var t=this.dropdown.querySelector(".focus"),s=a(this.dropdown,"open");if(13==e.keyCode)i(s?t:this.dropdown);else if(40==e.keyCode){if(s){var o=this._findNext(t);o&&(p(this.dropdown.querySelector(".focus"),"focus"),c(o,"focus"))}else i(this.dropdown);e.preventDefault()}else if(38==e.keyCode){if(s){var n=this._findPrev(t);n&&(p(this.dropdown.querySelector(".focus"),"focus"),c(n,"focus"))}else i(this.dropdown);e.preventDefault()}else if(27==e.keyCode&&s)i(this.dropdown);else if(32===e.keyCode&&s)return!1;return!1},u.prototype._findNext=function(e){for(e=e?e.nextElementSibling:this.dropdown.querySelector(".list .option");e;){if(!a(e,"disabled")&&"none"!=e.style.display)return e;e=e.nextElementSibling}return null},u.prototype._findPrev=function(e){for(e=e?e.previousElementSibling:this.dropdown.querySelector(".list .option:last-child");e;){if(!a(e,"disabled")&&"none"!=e.style.display)return e;e=e.previousElementSibling}return null},u.prototype._onSearchChanged=function(e){var t=a(this.dropdown,"open"),i=e.target.value;if(""==(i=i.toLowerCase()))this.options.forEach((function(e){e.element.style.display=""}));else if(t){var s=new RegExp(i);this.options.forEach((function(e){var t=e.data.text.toLowerCase(),i=s.test(t);e.element.style.display=i?"":"none"}))}this.dropdown.querySelectorAll(".focus").forEach((function(e){p(e,"focus")})),c(this._findNext(null),"focus")},t})())); \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index 3c20c799..0fe05859 100644 --- a/templates/header.html +++ b/templates/header.html @@ -1,7 +1,8 @@ {% load i18n static %} + {% if user.is_authenticated %}