diff --git a/car_inventory/settings.py b/car_inventory/settings.py index 221842b5..8750e315 100644 --- a/car_inventory/settings.py +++ b/car_inventory/settings.py @@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-gc9bh4*3=b6hihdnaom0edjsbxh$5t)aap@e8p&340r7)*)qb8 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['10.10.1.109', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] +ALLOWED_HOSTS = ['10.10.1.120', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] # Application definition @@ -111,9 +111,9 @@ WSGI_APPLICATION = 'car_inventory.wsgi.application' DATABASES = { "default": { "ENGINE": "django_prometheus.db.backends.postgresql", - "NAME": "haikal_app", - "USER": "f95166", - "PASSWORD": "Kfsh&rc9788", + "NAME": "haikal", + "USER": "haikal", + "PASSWORD": "haikal", "HOST": "localhost", "PORT": 5432, } diff --git a/inventory/migrations/0041_salequotation_quotation_number.py b/inventory/migrations/0041_salequotation_quotation_number.py new file mode 100644 index 00000000..cf41b475 --- /dev/null +++ b/inventory/migrations/0041_salequotation_quotation_number.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.17 on 2024-12-22 08:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0040_additionalservices_display_name'), + ] + + operations = [ + migrations.AddField( + model_name='salequotation', + name='quotation_number', + field=models.CharField(default=1, max_length=10, unique=True), + preserve_default=False, + ), + ] diff --git a/inventory/migrations/0042_salequotation_entity_salequotation_is_approved.py b/inventory/migrations/0042_salequotation_entity_salequotation_is_approved.py new file mode 100644 index 00000000..96a71637 --- /dev/null +++ b/inventory/migrations/0042_salequotation_entity_salequotation_is_approved.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.17 on 2024-12-22 10:36 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + +class Migration(migrations.Migration): + + dependencies = [ + ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), + ('inventory', '0041_salequotation_quotation_number'), + ] + + operations = [ + migrations.AddField( + model_name='salequotation', + name='entity', + field=models.ForeignKey(default="cb12725d-3b89-4742-8668-05d825b0b1f0", on_delete=django.db.models.deletion.CASCADE, to='django_ledger.entitymodel'), + preserve_default=False, + ), + migrations.AddField( + model_name='salequotation', + name='is_approved', + field=models.BooleanField(default=False), + ), + ] diff --git a/inventory/migrations/0043_remove_salequotation_status.py b/inventory/migrations/0043_remove_salequotation_status.py new file mode 100644 index 00000000..a9b9d74d --- /dev/null +++ b/inventory/migrations/0043_remove_salequotation_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.17 on 2024-12-22 11:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0042_salequotation_entity_salequotation_is_approved'), + ] + + operations = [ + migrations.RemoveField( + model_name='salequotation', + name='status', + ), + ] diff --git a/inventory/mixins.py b/inventory/mixins.py index b5b5b71e..a7afea2e 100644 --- a/inventory/mixins.py +++ b/inventory/mixins.py @@ -36,7 +36,7 @@ class LocalizedNameMixin: class AddDealerInstanceMixin: def form_valid(self, form): if form.is_valid(): - form.instance.dealer = self.request.user.dealer.get_parent_or_self + form.instance.dealer = self.request.user.dealer.get_root_dealer form.save() return super().form_valid(form) else: diff --git a/inventory/models.py b/inventory/models.py index 06d513b9..3ce243c4 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1,3 +1,4 @@ +import itertools from uuid import uuid4 from django.conf import settings from django.db import models, transaction @@ -24,9 +25,9 @@ from phonenumber_field.modelfields import PhoneNumberField from django.contrib.contenttypes.models import ContentType from django.utils.timezone import now from .utilities.financials import get_financial_value, get_total, get_total_financials - +from django.db.models import FloatField from .mixins import LocalizedNameMixin - +from django_ledger.models import EntityModel class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) @@ -538,7 +539,7 @@ class Dealer(models.Model, LocalizedNameMixin): def is_parent(self): return self.dealer_type == "Owner" @property - def get_parent_or_self(self): + def get_root_dealer(self): return self.parent_dealer if self.parent_dealer else self # Vendor Model @@ -602,6 +603,8 @@ class Customer(models.Model): @property def get_full_name(self): return f"{self.first_name} {self.middle_name} {self.last_name}" + + class Organization(models.Model, LocalizedNameMixin): @@ -639,6 +642,8 @@ class Representative(models.Model, LocalizedNameMixin): class SaleQuotation(models.Model): + quotation_number = models.CharField(max_length=10, unique=True) + STATUS_CHOICES = [ ("DRAFT", _("Draft")), ("CONFIRMED", _("Confirmed")), @@ -647,6 +652,7 @@ class SaleQuotation(models.Model): dealer = models.ForeignKey( Dealer, on_delete=models.CASCADE, related_name="sales", null=True ) + entity = models.ForeignKey(EntityModel, on_delete=models.CASCADE) customer = models.ForeignKey( Customer, on_delete=models.CASCADE, @@ -660,9 +666,10 @@ class SaleQuotation(models.Model): verbose_name=_("Amount"), ) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) - status = models.CharField( - max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status") - ) + is_approved = models.BooleanField(default=False) + # status = models.CharField( + # max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status") + # ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @@ -671,30 +678,53 @@ class SaleQuotation(models.Model): total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total'] return total_quantity or 0 - # @property - # def total(self): - # total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) - # if total: - # return float(total["total_price"]) * 0.15 + float(total["total_price"]) - # return 0 + @property + def total(self): + total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) + if not total: + return 0 + return total["total_price"] + + @property + def total_vat(self): + if self.total: + return float(self.total) * 0.15 + float(self.total) + return 0 - def confirm(self): - """Confirm the quotation and lock financial details.""" - if self.status != "DRAFT": - raise ValueError(_("Only draft quotations can be confirmed.")) - self.status = "CONFIRMED" - self.save() + # def confirm(self): + # """Confirm the quotation and lock financial details.""" + # if self.status != "DRAFT": + # raise ValueError(_("Only draft quotations can be confirmed.")) + # self.status = "CONFIRMED" + # self.save() - def cancel(self): - """Cancel the quotation.""" - if self.status == "CONFIRMED": - raise ValueError(_("Cannot cancel a confirmed quotation.")) - self.status = "CANCELED" - self.save() + # def cancel(self): + # """Cancel the quotation.""" + # if self.status == "CONFIRMED": + # raise ValueError(_("Cannot cancel a confirmed quotation.")) + # self.status = "CANCELED" + # self.save() def __str__(self): - return f"Quotation #{self.id} for {self.customer}" + return f"Quotation #{self.quotation_number} for {self.customer}" + + @property + def display_quotation_number(self): + return f"QN-{self.quotation_number}" + + def save(self, *args, **kwargs): + if not self.quotation_number: + self.quotation_number = str(next(self._get_quotation_number())).zfill(6) + super().save(*args, **kwargs) + @classmethod + def _get_quotation_number(cls): + last_quotation = cls.objects.all().order_by('id').last() + if last_quotation: + last_quotation_number = int(last_quotation.quotation_number) + else: + last_quotation_number = 0 + return itertools.count(last_quotation_number + 1) class SaleQuotationCar(models.Model): quotation = models.ForeignKey( @@ -733,14 +763,23 @@ class SaleQuotationCar(models.Model): # "total_amount": car_finance.total, } - # @property - # def total(self): - # """ - # Calculate total price dynamically based on quantity and selling price. - # """ - # if not self.car.finances: - # return Decimal("0.00") - # return self.car.finances.selling_price * self.quantity + @property + def total(self): + """ + Calculate total price dynamically based on quantity and selling price. + """ + if not self.car.finances: + return Decimal("0.00") + return self.car.finances.selling_price * self.quantity + @property + def total_vat(self): + """ + Calculate total price dynamically based on quantity and selling price. + """ + if not self.car.finances: + return Decimal("0.00") + price = float(self.car.finances.selling_price * self.quantity) + return (price * 0.15) + price def __str__(self): return f"{self.car} - Quotation #{self.quotation.id}" diff --git a/inventory/signals.py b/inventory/signals.py index 50242417..a0bc8f9f 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -15,11 +15,19 @@ from django.utils.translation import gettext_lazy as _ from . import models -@receiver(pre_delete, sender=models.Dealer) -def remove_user_account(sender, instance, **kwargs): - user = instance.user - if user: - user.delete() + +# @receiver(post_save, sender=models.SaleQuotation) +# def link_quotation_to_entity(sender, instance, created, **kwargs): +# if created: +# # Get the corresponding Django Ledger entity for the dealer +# entity = EntityModel.objects.get(name=instance.dealer.get_root_dealer.name) +# instance.entity = entity +# instance.save() +# @receiver(pre_delete, sender=models.Dealer) +# def remove_user_account(sender, instance, **kwargs): +# user = instance.user +# if user: +# user.delete() @receiver(post_save, sender=models.Car) def create_car_location(sender, instance, created, **kwargs): """ @@ -61,18 +69,23 @@ def update_car_status_on_reservation_delete(sender, instance, **kwargs): @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): if created: - entity = EntityModel.objects.create( - name=instance.name, - admin=instance.user, + entity, created = EntityModel.objects.get_or_create( + name=instance.get_root_dealer.name, + admin=instance.get_root_dealer.user, # address_1=instance.address, accrual_method=False, fy_start_month=1, # depth=0, ) - - default_coa = entity.create_chart_of_accounts(assign_as_default=True, + print(entity) + if created: + default_coa = entity.create_chart_of_accounts(assign_as_default=True, commit=True, coa_name=_("Chart of Accounts")) + if default_coa: + entity.populate_default_coa(activate_accounts=True, coa_model=default_coa) + print(f"Ledger entity created for Dealer: {instance.name}") + # entity.create_account( # coa_model=coa, # code=1010, @@ -120,15 +133,12 @@ def create_ledger_entity(sender, instance, created, **kwargs): # active=True) - if default_coa: - entity.populate_default_coa(activate_accounts=True, coa_model=default_coa) - + # uom_name = _("Unit") # unit_abbr = _("U") # # entity.create_uom(uom_name, unit_abbr) - print(f"Ledger entity created for Dealer: {instance.name}") # Create Vendor @@ -157,21 +167,21 @@ def create_ledger_vendor(sender, instance, created, **kwargs): @receiver(post_save, sender=models.Customer) def create_customer(sender, instance, created, **kwargs): - if created: - entity = EntityModel.objects.filter(name=instance.dealer.name).first() + entity = EntityModel.objects.filter(name=instance.dealer.get_root_dealer.name).first() name = f"{instance.first_name} {instance.middle_name} {instance.last_name}" - entity.create_customer( - customer_name=name, - customer_number=instance.national_id, - address_1=instance.address, - phone=instance.phone_number, - email=instance.email, - sales_tax_rate=0.15, - active=True, - hidden=False, - additional_info={} + entity.create_customer( + customer_model_kwargs={ + "customer_name": name, + "address_1": instance.address, + "phone": instance.phone_number, + "email": instance.email, + "sales_tax_rate": 0.15, + "active": True, + "hidden": False, + "additional_info": {} + } ) print(f"Customer created: {name}") diff --git a/inventory/urls.py b/inventory/urls.py index 4c261a14..c55dcf6e 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -73,6 +73,9 @@ urlpatterns = [ path('sales/quotations/', views.QuotationListView.as_view(), name='quotation_list'), path('sales/quotations//confirm/', views.confirm_quotation, name='confirm_quotation'), path('sales/orders/detail//', views.SalesOrderDetailView.as_view(), name='order_detail'), + path('quotation//pdf/', views.quotation_pdf_view, name='quotation_pdf'), + path('generate_invoice//', views.generate_invoice, name='generate_invoice'), + # Users URLs path('user/create/', views.UserCreateView.as_view(), name='user_create'), diff --git a/inventory/views.py b/inventory/views.py index 9b064173..27a1f1c7 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,7 +1,13 @@ +from django_ledger.models import EntityModel, InvoiceModel import logging import json from decimal import Decimal +from django.shortcuts import HttpResponse +from django.template.loader import render_to_string +from weasyprint import HTML +# from weasyprint.fonts import FontConfiguration + from django.views.decorators.csrf import csrf_exempt from vin import VIN from django.contrib.auth.mixins import LoginRequiredMixin @@ -17,7 +23,7 @@ from django.views.generic import ( CreateView, UpdateView, DeleteView, - TemplateView + TemplateView, ) from django.utils import timezone, translation from django.conf import settings @@ -29,16 +35,24 @@ from django.db.models import Sum, F, Count from inventory.mixins import AddDealerInstanceMixin -from .services import elm, decodevin,get_make,get_model,normalize_name -from .services import elm, decodevin, get_make, get_model, normalize_name, get_ledger_data +from .services import elm, decodevin, get_make, get_model, normalize_name +from .services import ( + elm, + decodevin, + get_make, + get_model, + normalize_name, + get_ledger_data, +) from . import models, forms from django_tables2.export.views import ExportMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.decorators import user_passes_test from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.models import Group -from django.contrib.auth import get_user_model +from django.contrib.auth import get_user_model from .utils import get_calculations + User = get_user_model() logger = logging.getLogger(__name__) @@ -46,17 +60,24 @@ logging.basicConfig(level=logging.INFO) def switch_language(request): - language = request.GET.get('language', 'en') - referer = request.META.get('HTTP_REFERER', '/') + language = request.GET.get("language", "en") + referer = request.META.get("HTTP_REFERER", "/") parsed_url = urlparse(referer) - path_parts = parsed_url.path.split('/') + path_parts = parsed_url.path.split("/") if path_parts[1] in dict(settings.LANGUAGES): path_parts.pop(1) - new_path = '/'.join(path_parts) + new_path = "/".join(path_parts) new_url = urlunparse( - (parsed_url.scheme, parsed_url.netloc, new_path, parsed_url.params, parsed_url.query, parsed_url.fragment) + ( + parsed_url.scheme, + parsed_url.netloc, + new_path, + parsed_url.params, + parsed_url.query, + parsed_url.fragment, + ) ) if language in dict(settings.LANGUAGES): @@ -65,41 +86,47 @@ def switch_language(request): response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language) translation.activate(language) request.session[settings.LANGUAGE_COOKIE_NAME] = language - logger.debug(f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}") + logger.debug( + f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}" + ) return response else: logger.warning(f"Invalid language code: {language}") - return redirect('/') + return redirect("/") class HomeView(LoginRequiredMixin, TemplateView): - template_name = 'index.html' - + template_name = "index.html" def dispatch(self, request, *args, **kwargs): - if not any(hasattr(request.user, attr) for attr in ['dealer', 'subdealer']) or not request.user.is_authenticated: - messages.error(request, _('You are not associated with any dealer.')) - return redirect('welcome') + if ( + not any(hasattr(request.user, attr) for attr in ["dealer", "subdealer"]) + or not request.user.is_authenticated + ): + messages.error(request, _("You are not associated with any dealer.")) + return redirect("welcome") return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) total_cars = models.Car.objects.count() - total_reservations = models.CarReservation.objects.filter(reserved_until__gte=timezone.now()).count() + total_reservations = models.CarReservation.objects.filter( + reserved_until__gte=timezone.now() + ).count() stats = models.CarFinance.objects.aggregate( - total_cost_price=Sum('cost_price'), - total_selling_price=Sum('selling_price'), + total_cost_price=Sum("cost_price"), + total_selling_price=Sum("selling_price"), ) - total_cost_price = stats['total_cost_price'] or 0 - total_selling_price = stats['total_selling_price'] or 0 + total_cost_price = stats["total_cost_price"] or 0 + total_selling_price = stats["total_selling_price"] or 0 total_profit = total_selling_price - total_cost_price - context['total_cars'] = total_cars - context['total_reservations'] = total_reservations - context['total_cost_price'] = total_cost_price - context['total_selling_price'] = total_selling_price - context['total_profit'] = total_profit + context["total_cars"] = total_cars + context["total_reservations"] = total_reservations + context["total_cost_price"] = total_cost_price + context["total_selling_price"] = total_selling_price + context["total_profit"] = total_profit return context @@ -110,157 +137,176 @@ class WelcomeView(TemplateView): class CarCreateView(LoginRequiredMixin, CreateView): model = models.Car form_class = forms.CarForm - template_name = 'inventory/car_form.html' + template_name = "inventory/car_form.html" # success_url = reverse_lazy('inventory_stats') def get_success_url(self): """Determine the redirect URL based on user choice.""" - if self.request.POST.get('add_another'): - return reverse('car_add') - return reverse('inventory_stats') + if self.request.POST.get("add_another"): + return reverse("car_add") + return reverse("inventory_stats") def form_valid(self, form): - form.instance.dealer = self.request.user.dealer.get_parent_or_self + form.instance.dealer = self.request.user.dealer.get_root_dealer form.save() - messages.success(self.request, 'Car saved successfully.') + messages.success(self.request, "Car saved successfully.") return super().form_valid(form) class AjaxHandlerView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): - action = request.GET.get('action') + action = request.GET.get("action") handlers = { - 'decode_vin': self.decode_vin, - 'get_models': self.get_models, - 'get_series': self.get_series, - 'get_trims': self.get_trims, - 'get_specifications': self.get_specifications, + "decode_vin": self.decode_vin, + "get_models": self.get_models, + "get_series": self.get_series, + "get_trims": self.get_trims, + "get_specifications": self.get_specifications, } handler = handlers.get(action) if handler: return handler(request) else: - return JsonResponse({'error': 'Invalid action'}, status=400) + return JsonResponse({"error": "Invalid action"}, status=400) def decode_vin(self, request): - vin_no = request.GET.get('vin_no') + vin_no = request.GET.get("vin_no") if not vin_no or len(vin_no.strip()) != 17: - return JsonResponse({'success': False, 'error': 'Invalid VIN number provided.'}, status=400) - + return JsonResponse( + {"success": False, "error": "Invalid VIN number provided."}, status=400 + ) + vin_no = vin_no.strip() vin_data = {} - decoding_method = '' + decoding_method = "" # manufacturer_name = model_name = year_model = None - if not (result :=decodevin(vin_no)): - return JsonResponse({'success': False, 'error': 'VIN not found in all sources.'}, status=404) - - manufacturer_name,model_name,year_model = result.values() + if not (result := decodevin(vin_no)): + return JsonResponse( + {"success": False, "error": "VIN not found in all sources."}, status=404 + ) + + manufacturer_name, model_name, year_model = result.values() make = get_make(manufacturer_name) - model = get_model(model_name,make) - + model = get_model(model_name, make) + logger.info( f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}" - ) + ) car_model = model car_make = make - + if not car_make: - return JsonResponse({'success': False, 'error': 'Manufacturer not found in the database.'}, status=404) - vin_data['make_id'] = car_make.id_car_make - vin_data['name'] = car_make.name - vin_data['arabic_name'] = car_make.arabic_name - + return JsonResponse( + {"success": False, "error": "Manufacturer not found in the database."}, + status=404, + ) + vin_data["make_id"] = car_make.id_car_make + vin_data["name"] = car_make.name + vin_data["arabic_name"] = car_make.arabic_name + if not car_model: - vin_data['model_id'] = "" + vin_data["model_id"] = "" else: - vin_data['model_id'] = car_model.id_car_model - vin_data['year'] = year_model - return JsonResponse({'success': True, 'data': vin_data}) + vin_data["model_id"] = car_model.id_car_model + vin_data["year"] = year_model + return JsonResponse({"success": True, "data": vin_data}) def get_models(self, request): - make_id = request.GET.get('make_id') - car_models = (models.CarModel.objects.filter(id_car_make=make_id) - .values('id_car_model', 'name', 'arabic_name') - .order_by('name')) + make_id = request.GET.get("make_id") + car_models = ( + models.CarModel.objects.filter(id_car_make=make_id) + .values("id_car_model", "name", "arabic_name") + .order_by("name") + ) return JsonResponse(list(car_models), safe=False) def get_series(self, request): - model_id = request.GET.get('model_id') - year = request.GET.get('year') - + model_id = request.GET.get("model_id") + year = request.GET.get("year") # Validate inputs if not model_id or not year: - return JsonResponse({'error': 'Missing required parameters: model_id or year'}, status=400) + return JsonResponse( + {"error": "Missing required parameters: model_id or year"}, status=400 + ) try: year = int(year) except ValueError: - return JsonResponse({'error': 'Invalid year format'}, status=400) + return JsonResponse({"error": "Invalid year format"}, status=400) - series = models.CarSerie.objects.filter( - id_car_model=model_id).values( - 'id_car_serie', 'name', 'arabic_name' + series = models.CarSerie.objects.filter(id_car_model=model_id).values( + "id_car_serie", "name", "arabic_name" ) return JsonResponse(list(series), safe=False) def get_trims(self, request): - serie_id = request.GET.get('serie_id') + serie_id = request.GET.get("serie_id") # model_id = request.GET.get('model_id') - trims = models.CarTrim.objects.filter( - id_car_serie=serie_id).values('id_car_trim', 'name', 'arabic_name') + trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values( + "id_car_trim", "name", "arabic_name" + ) return JsonResponse(list(trims), safe=False) def get_specifications(self, request): - trim_id = request.GET.get('trim_id') - car_spec_values = models.CarSpecificationValue.objects.filter(id_car_trim=trim_id) + trim_id = request.GET.get("trim_id") + car_spec_values = models.CarSpecificationValue.objects.filter( + id_car_trim=trim_id + ) lang = translation.get_language() specs_by_parent = {} for value in car_spec_values: specification = value.id_car_specification parent = specification.id_parent parent_id = parent.id_car_specification if parent else 0 - if lang == 'ar': + if lang == "ar": parent_name = parent.arabic_name if parent else "Root" else: parent_name = parent.name if parent else "Root" if parent_id not in specs_by_parent: - specs_by_parent[parent_id] = {'parent_name': parent_name, 'specifications': []} + specs_by_parent[parent_id] = { + "parent_name": parent_name, + "specifications": [], + } spec_data = { - 'specification_id': specification.id_car_specification, - 's_name': specification.arabic_name if lang == 'ar' else specification.name, - 's_value': value.value, - 's_unit': value.unit if value.unit else "", - 'trim_name': value.id_car_trim.name + "specification_id": specification.id_car_specification, + "s_name": specification.arabic_name + if lang == "ar" + else specification.name, + "s_value": value.value, + "s_unit": value.unit if value.unit else "", + "trim_name": value.id_car_trim.name, } - specs_by_parent[parent_id]['specifications'].append(spec_data) + specs_by_parent[parent_id]["specifications"].append(spec_data) serialized_specs = [ - {'parent_name': v['parent_name'], 'specifications': v['specifications']} + {"parent_name": v["parent_name"], "specifications": v["specifications"]} for v in specs_by_parent.values() ] return JsonResponse(serialized_specs, safe=False) + class CarInventory(LoginRequiredMixin, ListView): model = models.Car - home_label = _('inventory') - template_name = 'inventory/car_inventory.html' - context_object_name = 'cars' + home_label = _("inventory") + template_name = "inventory/car_inventory.html" + context_object_name = "cars" paginate_by = 10 - ordering = ['receiving_date'] + ordering = ["receiving_date"] def get_queryset(self, *args, **kwargs): - query = self.request.GET.get('q') - make_id = self.kwargs['make_id'] - model_id = self.kwargs['model_id'] - trim_id = self.kwargs['trim_id'] - + query = self.request.GET.get("q") + make_id = self.kwargs["make_id"] + model_id = self.kwargs["model_id"] + trim_id = self.kwargs["trim_id"] + cars = models.Car.objects.filter( - dealer=self.request.user.dealer.get_parent_or_self, + dealer=self.request.user.dealer.get_root_dealer, id_car_make=make_id, id_car_model=model_id, - id_car_trim=trim_id,).order_by('receiving_date') + id_car_trim=trim_id, + ).order_by("receiving_date") if query: cars = cars.filter(Q(vin__icontains=query)) @@ -268,10 +314,10 @@ class CarInventory(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['query'] = self.request.GET.get('q', '') - context['make_id'] = self.kwargs['make_id'] - context['model_id'] = self.kwargs['model_id'] - context['trim_id'] = self.kwargs['trim_id'] + context["query"] = self.request.GET.get("q", "") + context["make_id"] = self.kwargs["make_id"] + context["model_id"] = self.kwargs["model_id"] + context["trim_id"] = self.kwargs["trim_id"] return context @@ -281,16 +327,16 @@ class CarColorCreate(LoginRequiredMixin, CreateView): template_name = "inventory/add_colors.html" def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) form.instance.car = car return super().form_valid(form) def get_success_url(self): - return reverse_lazy('car_detail', kwargs={'pk': self.kwargs['car_pk']}) + return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['car'] = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return context @@ -300,12 +346,12 @@ def inventory_stats_view(request): # Annotate total cars by make, model, and trim cars = ( - models.Car.objects.filter(dealer=dealer.get_parent_or_self) - .select_related('id_car_make', 'id_car_model', 'id_car_trim') + models.Car.objects.filter(dealer=dealer.get_root_dealer) + .select_related("id_car_make", "id_car_model", "id_car_trim") .annotate( - make_total=Count('id_car_make'), - model_total=Count('id_car_model'), - trim_total=Count('id_car_trim') + make_total=Count("id_car_make"), + model_total=Count("id_car_model"), + trim_total=Count("id_car_trim"), ) ) @@ -316,145 +362,150 @@ def inventory_stats_view(request): make = car.id_car_make if make.id_car_make not in inventory: inventory[make.id_car_make] = { - 'make_id': make.id_car_make, - 'make_name': make.get_local_name(), - 'total_cars': 0, - 'models': {} + "make_id": make.id_car_make, + "make_name": make.get_local_name(), + "total_cars": 0, + "models": {}, } - inventory[make.id_car_make]['total_cars'] += 1 + inventory[make.id_car_make]["total_cars"] += 1 # Model Level model = car.id_car_model - if model and model.id_car_model not in inventory[make.id_car_make]['models']: - inventory[make.id_car_make]['models'][model.id_car_model] = { - 'model_id': model.id_car_model, - 'model_name': model.get_local_name(), - 'total_cars': 0, - 'trims': {} + if model and model.id_car_model not in inventory[make.id_car_make]["models"]: + inventory[make.id_car_make]["models"][model.id_car_model] = { + "model_id": model.id_car_model, + "model_name": model.get_local_name(), + "total_cars": 0, + "trims": {}, } - inventory[make.id_car_make]['models'][model.id_car_model]['total_cars'] += 1 + inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1 # Trim Level trim = car.id_car_trim - if trim and trim.id_car_trim not in inventory[make.id_car_make]['models'][model.id_car_model]['trims']: - inventory[make.id_car_make]['models'][model.id_car_model]['trims'][trim.id_car_trim] = { - 'trim_id': trim.id_car_trim, - 'trim_name': trim.name, - 'total_cars': 0 - } - inventory[make.id_car_make]['models'][model.id_car_model]['trims'][trim.id_car_trim]['total_cars'] += 1 + if ( + trim + and trim.id_car_trim + not in inventory[make.id_car_make]["models"][model.id_car_model]["trims"] + ): + inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ + trim.id_car_trim + ] = {"trim_id": trim.id_car_trim, "trim_name": trim.name, "total_cars": 0} + inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ + trim.id_car_trim + ]["total_cars"] += 1 # Convert to a list for easier template rendering result = { - 'total_cars': cars.count(), - 'makes': [ + "total_cars": cars.count(), + "makes": [ { - 'make_id': make_data['make_id'], - 'make_name': make_data['make_name'], - 'total_cars': make_data['total_cars'], - 'models': [ + "make_id": make_data["make_id"], + "make_name": make_data["make_name"], + "total_cars": make_data["total_cars"], + "models": [ { - 'model_id': model_data['model_id'], - 'model_name': model_data['model_name'], - 'total_cars': model_data['total_cars'], - 'trims': list(model_data['trims'].values()) + "model_id": model_data["model_id"], + "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() - ] + for model_data in make_data["models"].values() + ], } for make_data in inventory.values() - ] + ], } - return render(request, 'inventory/inventory_stats.html', {'inventory': result}) + return render(request, "inventory/inventory_stats.html", {"inventory": result}) class CarDetailView(LoginRequiredMixin, DetailView): model = models.Car - template_name = 'inventory/car_detail.html' - context_object_name = 'car' + template_name = "inventory/car_detail.html" + context_object_name = "car" + class CarFinanceCreateView(LoginRequiredMixin, CreateView): model = models.CarFinance form_class = forms.CarFinanceForm - template_name = 'inventory/car_finance_form.html' + template_name = "inventory/car_finance_form.html" def dispatch(self, request, *args, **kwargs): - self.car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): form.instance.car = self.car - messages.success(self.request, _('Car finance details saved successfully.')) + messages.success(self.request, _("Car finance details saved successfully.")) return super().form_valid(form) def get_success_url(self): - return reverse('car_detail', kwargs={'pk': self.car.pk}) + return reverse("car_detail", kwargs={"pk": self.car.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['car'] = self.car + context["car"] = self.car return context -class CarFinanceUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): +class CarFinanceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.CarFinance form_class = forms.CarFinanceForm - template_name = 'inventory/car_finance_form.html' - success_message = _('Car finance details updated successfully.') + template_name = "inventory/car_finance_form.html" + success_message = _("Car finance details updated successfully.") def get_success_url(self): - return reverse('car_detail', kwargs={'pk': self.object.car.pk}) - + return reverse("car_detail", kwargs={"pk": self.object.car.pk}) + def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['instance'] = self.get_object() + kwargs["instance"] = self.get_object() return kwargs -class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin,UpdateView): +class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.Car form_class = forms.CarUpdateForm - template_name = 'inventory/car_edit.html' - success_message = _('Car updated successfully.') - + template_name = "inventory/car_edit.html" + success_message = _("Car updated successfully.") + def get_success_url(self): - return reverse('car_detail', kwargs={'pk': self.object.pk}) + return reverse("car_detail", kwargs={"pk": self.object.pk}) -class CarDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): +class CarDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = models.Car - template_name = 'inventory/car_confirm_delete.html' - success_url = reverse_lazy('inventory_stats') + template_name = "inventory/car_confirm_delete.html" + success_url = reverse_lazy("inventory_stats") def delete(self, request, *args, **kwargs): - messages.success(request, _('Car deleted successfully.')) + messages.success(request, _("Car deleted successfully.")) return super().delete(request, *args, **kwargs) class CarLocationCreateView(CreateView): model = models.CarLocation form_class = forms.CarLocationForm - template_name = 'inventory/car_location_form.html' + template_name = "inventory/car_location_form.html" def get_success_url(self): - return reverse_lazy('car_detail', kwargs={'pk': self.object.car.pk}) + return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) def form_valid(self, form): - form.instance.car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) form.instance.owner = self.request.user.dealer form.save() - messages.success(self.request, 'Car saved successfully.') + messages.success(self.request, "Car saved successfully.") return super().form_valid(form) class CarLocationUpdateView(UpdateView): model = models.CarLocation form_class = forms.CarLocationForm - template_name = 'inventory/car_location_form.html' + template_name = "inventory/car_location_form.html" def get_success_url(self): - return reverse_lazy('car_detail', kwargs={'pk': self.object.car.pk}) + return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) class CustomCardCreateView(LoginRequiredMixin, CreateView): @@ -463,18 +514,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView): template_name = "inventory/add_custom_card.html" def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) form.instance.car = car return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['car'] = get_object_or_404(models.Car, pk=self.kwargs['car_pk']) + context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return context def get_success_url(self): messages.success(self.request, _("Custom Card added successfully.")) - return reverse_lazy('car_detail', kwargs={'pk': self.kwargs['car_pk']}) + return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) @login_required() @@ -483,26 +534,28 @@ def reserve_car_view(request, car_id): car = get_object_or_404(models.Car, pk=car_id) if car.is_reserved(): messages.error(request, _("This car is already reserved.")) - return redirect('car_detail', pk=car.pk) + return redirect("car_detail", pk=car.pk) try: reserved_until = timezone.now() + timezone.timedelta(hours=24) models.CarReservation.objects.create( - car=car, - reserved_by=request.user, - reserved_until=reserved_until + car=car, reserved_by=request.user, reserved_until=reserved_until ) messages.success(request, _("Car reserved successfully.")) except Exception as e: messages.error(request, f"Error reserving car: {e}") - return redirect('car_detail', pk=car.pk) - return JsonResponse({"success": False, "message": "Invalid request method."}, status=400) + return redirect("car_detail", pk=car.pk) + return JsonResponse( + {"success": False, "message": "Invalid request method."}, status=400 + ) @login_required def manage_reservation(request, reservation_id): - reservation = get_object_or_404(models.CarReservation, pk=reservation_id, reserved_by=request.user) + reservation = get_object_or_404( + models.CarReservation, pk=reservation_id, reserved_by=request.user + ) if request.method == "POST": action = request.POST.get("action") @@ -510,170 +563,211 @@ def manage_reservation(request, reservation_id): reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24) reservation.save() messages.success(request, _("Reservation renewed successfully.")) - return redirect('car_detail', pk=reservation.car.pk) + return redirect("car_detail", pk=reservation.car.pk) elif action == "cancel": reservation.delete() messages.success(request, _("Reservation canceled successfully.")) - return redirect('car_detail', pk=reservation.car.pk) + return redirect("car_detail", pk=reservation.car.pk) else: - return JsonResponse({"success": False, "message": _("Invalid action.")}, status=400) + return JsonResponse( + {"success": False, "message": _("Invalid action.")}, status=400 + ) - return JsonResponse({"success": False, "message": _("Invalid request method.")}, status=400) + return JsonResponse( + {"success": False, "message": _("Invalid request method.")}, status=400 + ) class DealerListView(LoginRequiredMixin, ListView): model = models.Dealer - template_name = 'dealer_list.html' - context_object_name = 'dealers' + template_name = "dealer_list.html" + context_object_name = "dealers" class DealerDetailView(LoginRequiredMixin, DetailView): model = models.Dealer - template_name = 'dealers/dealer_detail.html' - context_object_name = 'dealer' + template_name = "dealers/dealer_detail.html" + context_object_name = "dealer" -class DealerCreateView(LoginRequiredMixin, SuccessMessageMixin,CreateView): +class DealerCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = models.Dealer form_class = forms.DealerForm - template_name = 'dealer_form.html' - success_url = reverse_lazy('dealer_list') - success_message = _('Dealer created successfully.') + template_name = "dealer_form.html" + success_url = reverse_lazy("dealer_list") + success_message = _("Dealer created successfully.") -class DealerUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): + +class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.Dealer form_class = forms.DealerForm - template_name = 'dealers/dealer_form.html' - success_url = reverse_lazy('dealer_detail') - success_message = _('Dealer updated successfully.') - + template_name = "dealers/dealer_form.html" + success_url = reverse_lazy("dealer_detail") + success_message = _("Dealer updated successfully.") + def get_success_url(self): - return reverse('dealer_detail', kwargs={'pk': self.object.pk}) - + 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') + 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': + if self.request.user.dealer.dealer_type == "Owner": return forms.DealerForm else: return forms.UserForm - -class DealerDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): - model = models.Dealer - template_name = 'dealer_confirm_delete.html' - success_url = reverse_lazy('dealer_list') - success_message = _('Dealer deleted successfully.') -class CustomerListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): + +class DealerDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): + model = models.Dealer + template_name = "dealer_confirm_delete.html" + success_url = reverse_lazy("dealer_list") + success_message = _("Dealer deleted successfully.") + + +class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = models.Customer - home_label = _('customers') - context_object_name = 'customers' + home_label = _("customers") + context_object_name = "customers" paginate_by = 10 template_name = "customers/customer_list.html" - permission_required = ('inventory.view_customer',) + permission_required = ("inventory.view_customer",) def get_queryset(self): - query = self.request.GET.get('q') - customers = models.Customer.objects.filter(dealer=self.request.user.dealer.get_parent_or_self) + query = self.request.GET.get("q") + customers = models.Customer.objects.filter( + dealer=self.request.user.dealer.get_root_dealer + ) if query: customers = customers.filter( - Q(national_id__icontains=query) | - Q(first_name__icontains=query) | - Q(last_name__icontains=query) + Q(national_id__icontains=query) + | Q(first_name__icontains=query) + | Q(last_name__icontains=query) ) return customers def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['query'] = self.request.GET.get('q', '') + context["query"] = self.request.GET.get("q", "") return context -class CustomerDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): - model = models.Customer - template_name = 'customers/view_customer.html' - context_object_name = 'customer' - permission_required = ('inventory.view_customer',) -class CustomerCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): +class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): + model = models.Customer + template_name = "customers/view_customer.html" + context_object_name = "customer" + permission_required = ("inventory.view_customer",) + + +class CustomerCreateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + CreateView, +): model = models.Customer form_class = forms.CustomerForm - template_name = 'customers/customer_form.html' - success_url = reverse_lazy('customer_list') - permission_required = ('inventory.add_customer',) - success_message = _('Customer created successfully.') + template_name = "customers/customer_form.html" + success_url = reverse_lazy("customer_list") + permission_required = ("inventory.add_customer",) + success_message = _("Customer created successfully.") -class CustomerUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): + +class CustomerUpdateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + UpdateView, +): model = models.Customer form_class = forms.CustomerForm - template_name = 'customers/customer_form.html' - success_url = reverse_lazy('customer_list') - permission_required = ('inventory.change_customer',) - success_message = _('Customer updated successfully.') + template_name = "customers/customer_form.html" + success_url = reverse_lazy("customer_list") + permission_required = ("inventory.change_customer",) + success_message = _("Customer updated successfully.") + @login_required def delete_customer(request, pk): customer = get_object_or_404(models.Customer, pk=pk) customer.delete() - messages.success(request, _('Customer deleted successfully.')) - return redirect('customer_list') + messages.success(request, _("Customer deleted successfully.")) + return redirect("customer_list") -class VendorListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): +class VendorListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = models.Vendor - context_object_name = 'vendors' + context_object_name = "vendors" paginate_by = 10 template_name = "vendors/vendors_list.html" - permission_required = ('inventory.view_vendor',) + permission_required = ("inventory.view_vendor",) -class VendorDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): +class VendorDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.Vendor template_name = "vendors/view_vendor.html" - permission_required = ('inventory.view_vendor',) + permission_required = ("inventory.view_vendor",) -class VendorCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): +class VendorCreateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + CreateView, +): model = models.Vendor form_class = forms.VendorForm - template_name = 'vendors/vendor_form.html' - success_url = reverse_lazy('vendor_list') - permission_required = ('inventory.add_vendor',) - success_message = _('Vendor created successfully.') - -class VendorUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): + template_name = "vendors/vendor_form.html" + success_url = reverse_lazy("vendor_list") + permission_required = ("inventory.add_vendor",) + success_message = _("Vendor created successfully.") + + +class VendorUpdateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + UpdateView, +): model = models.Vendor form_class = forms.VendorForm - template_name = 'vendors/vendor_form.html' - success_url = reverse_lazy('vendor_list') - permission_required = ('inventory.change_vendor',) - success_message = _('Vendor updated successfully.') + template_name = "vendors/vendor_form.html" + success_url = reverse_lazy("vendor_list") + permission_required = ("inventory.change_vendor",) + success_message = _("Vendor updated successfully.") + @login_required def delete_vendor(request, pk): vendor = get_object_or_404(models.Vendor, pk=pk) vendor.delete() - messages.success(request, _('Vendor deleted successfully.')) - return redirect('vendor_list') + messages.success(request, _("Vendor deleted successfully.")) + return redirect("vendor_list") -class QuotationCreateView(LoginRequiredMixin,PermissionRequiredMixin, CreateView): +class QuotationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): model = models.SaleQuotation form_class = forms.QuotationForm - template_name = 'sales/quotation_form.html' - permission_required = ('inventory.add_salequotation',) - + template_name = "sales/quotation_form.html" + permission_required = ("inventory.add_salequotation",) def form_valid(self, form): - dealer = self.request.user.dealer.get_parent_or_self + dealer = self.request.user.dealer.get_root_dealer + entity = EntityModel.objects.get(name=dealer.get_root_dealer.name) form.instance.dealer = dealer + form.instance.entity = entity quotation = form.save() - selected_cars = form.cleaned_data.get("cars") + selected_cars = form.cleaned_data.get("cars") for car in selected_cars: car_finance = car.finances if car_finance: @@ -683,147 +777,226 @@ class QuotationCreateView(LoginRequiredMixin,PermissionRequiredMixin, CreateView ) messages.success(self.request, _("Quotation created successfully.")) - return redirect('quotation_list') + return redirect("quotation_list") -class QuotationListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): +class QuotationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = models.SaleQuotation template_name = "sales/quotation_list.html" context_object_name = "quotations" paginate_by = 10 - permission_required = ('inventory.view_salequotation',) - + permission_required = ("inventory.view_salequotation",) def get_queryset(self): status = self.request.GET.get("status") # queryset = models.SaleQuotation.objects.all() - print(self.request.user.dealer.get_parent_or_self.sales.all()) - queryset = self.request.user.dealer.get_parent_or_self.sales.all() + print(self.request.user.dealer.get_root_dealer.sales.all()) + queryset = self.request.user.dealer.get_root_dealer.sales.all() if status: queryset = queryset.filter(status=status) return queryset -class QuotationDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): +class QuotationDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.SaleQuotation template_name = "sales/quotation_detail.html" context_object_name = "quotation" - permission_required = ('inventory.view_salequotation',) - + permission_required = ("inventory.view_salequotation",) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) quotation = self.object - + context_result = get_calculations(quotation) - context.update(context_result) + context.update(context_result) return context +@login_required +def generate_invoice(request, pk): + quotation = get_object_or_404(models.SaleQuotation, pk=pk) + if not quotation.is_approved: + messages.error( + request, "Quotation must be approved before converting to an invoice." + ) + else: + entity = quotation.entity + customer = ( + entity.get_customers() + .filter(customer_name=quotation.customer.get_full_name) + .first() + ) + + invoices = entity.create_invoice( + customer_model=customer.customer_number, terms=InvoiceModel.TERMS_NET_30 + ) + # invoice_itemtxs = { + # f"{qc}": { + # "unit_cost": f"{qc.car.finances.selling_price}", + # "quantity": f"{qc.quantity}", + # "total_amount": f"{qc.total_vat}", + # } + # for qc in quotation.quotation_cars.all() + # } + invoice_itemtxs = { + "test":{ + "unit_cost": "1000", + "quantity": "1", + "total_amount": "1000", + }, + "test1":{ + "unit_cost": "1000", + "quantity": "1", + "total_amount": "1000", + } + } + invoice_itemtxs = invoices.migrate_itemtxs( + itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND + ) + 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 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_amount = quotation_cars.aggregate(total=Sum('total_price'))['total'] + # 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.quotation_cars.aggregate(Sum("total_amount"))["total_amount__sum"], + 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,PermissionRequiredMixin, DetailView): +class SalesOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.SalesOrder template_name = "sales/sales_order_detail.html" context_object_name = "sales_order" - permission_required = ('inventory.view_salequotation',) - slug_field = 'order_id' - slug_url_kwarg = 'order_id' - - -#Users -class UserListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): + permission_required = ("inventory.view_salequotation",) + slug_field = "order_id" + slug_url_kwarg = "order_id" + + +# Users +class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = models.Dealer - context_object_name = 'users' + context_object_name = "users" paginate_by = 10 template_name = "users/user_list.html" - permission_required = ('inventory.view_dealer',) + permission_required = ("inventory.view_dealer",) - def get_queryset(self): - query = self.request.GET.get('q') + def get_queryset(self): + query = self.request.GET.get("q") users = self.request.user.dealer.sub_dealers if query: users = users.filter( - Q(name__icontains=query) | - Q(arabic_name__icontains=query) | - Q(phone_number__icontains=query)| - Q(address__icontains=query) + Q(name__icontains=query) + | Q(arabic_name__icontains=query) + | Q(phone_number__icontains=query) + | Q(address__icontains=query) ) return users.all() - -class UserDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): + +class UserDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.Dealer template_name = "users/user_detail.html" context_object_name = "user_" - permission_required = ('inventory.view_dealer',) - -class UserCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): + permission_required = ("inventory.view_dealer",) + + +class UserCreateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + CreateView, +): model = models.Dealer form_class = forms.UserForm - template_name = 'users/user_form.html' - success_url = reverse_lazy('user_list') - permission_required = ('inventory.add_dealer',) - success_message = _('User created successfully.') - - def get_form(self, form_class = None): + template_name = "users/user_form.html" + success_url = reverse_lazy("user_list") + permission_required = ("inventory.add_dealer",) + success_message = _("User created successfully.") + + def get_form(self, form_class=None): form = super().get_form(form_class) - form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner'] + form.fields["dealer_type"].choices = [ + t for t in form.fields["dealer_type"].choices if t[0] != "Owner" + ] return form - def form_valid(self, form): - dealer = self.request.user.dealer.get_parent_or_self + + def form_valid(self, form): + dealer = self.request.user.dealer.get_root_dealer if dealer.sub_dealers.count() >= dealer.get_active_plan.max_users: - messages.error(self.request, _("You have reached the maximum number of users.")) - return redirect('user_list') - - user = User.objects.create_user(username=form.cleaned_data['name']) + messages.error( + self.request, _("You have reached the maximum number of users.") + ) + return redirect("user_list") + + user = User.objects.create_user(username=form.cleaned_data["name"]) user.set_password("Tenhal@123") user.save() form.instance.user = user form.instance.parent_dealer = dealer for group in user.groups.all(): group.user_set.remove(user) - Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user) + Group.objects.get(name=form.cleaned_data["dealer_type"].lower()).user_set.add( + user + ) form.save() return super().form_valid(form) -class UserUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): +class UserUpdateView( + LoginRequiredMixin, + PermissionRequiredMixin, + SuccessMessageMixin, + AddDealerInstanceMixin, + UpdateView, +): model = models.Dealer form_class = forms.UserForm - template_name = 'users/user_form.html' - success_url = reverse_lazy('user_list') - permission_required = ('inventory.change_dealer',) - success_message = _('User updated successfully.') - - def get_form(self, form_class = None): + template_name = "users/user_form.html" + success_url = reverse_lazy("user_list") + permission_required = ("inventory.change_dealer",) + success_message = _("User updated successfully.") + + def get_form(self, form_class=None): form = super().get_form(form_class) if not self.request.user.has_perms(["inventory.change_dealer_type"]): - field = form.fields['dealer_type'] + field = form.fields["dealer_type"] field.widget = field.hidden_widget() - form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner'] + form.fields["dealer_type"].choices = [ + t for t in form.fields["dealer_type"].choices if t[0] != "Owner" + ] return form - def form_valid(self, form): + + def form_valid(self, form): user = form.instance.user for group in user.groups.all(): group.user_set.remove(user) - Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user) + Group.objects.get(name=form.cleaned_data["dealer_type"].lower()).user_set.add( + user + ) form.save() return super().form_valid(form) @@ -831,102 +1004,133 @@ class UserUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMi def UserDeleteview(request, pk): user = get_object_or_404(models.Dealer, pk=pk) user.delete() - messages.success(request, _('User deleted successfully.')) - return redirect('user_list') + messages.success(request, _("User deleted successfully.")) + return redirect("user_list") -#errors +# errors def custom_page_not_found_view(request, exception): return render(request, "errors/404.html", {}) + def custom_error_view(request, exception=None): return render(request, "errors/500.html", {}) + def custom_permission_denied_view(request, exception=None): return render(request, "errors/403.html", {}) + def custom_bad_request_view(request, exception=None): return render(request, "errors/400.html", {}) class OrganizationListView(LoginRequiredMixin, ListView): model = models.Organization - template_name = 'organizations/organization_list.html' - context_object_name = 'organizations' + template_name = "organizations/organization_list.html" + context_object_name = "organizations" class OrganizationDetailView(DetailView): model = models.Organization - template_name = 'organizations/organization_detail.html' - context_object_name = 'organization' + template_name = "organizations/organization_detail.html" + context_object_name = "organization" -class OrganizationCreateView(LoginRequiredMixin,SuccessMessageMixin, CreateView): +class OrganizationCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = models.Organization form_class = forms.OrganizationForm - template_name = 'organizations/organization_form.html' - success_url = reverse_lazy('organization_list') + template_name = "organizations/organization_form.html" + success_url = reverse_lazy("organization_list") success_message = "Organization created successfully." - def form_valid(self, form): - if form.is_valid(): - form.instance.dealer = self.request.user.dealer.get_parent_or_self - form.save() - return super().form_valid(form) - else: - return form.errors - - -class OrganizationUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): - model = models.Organization - form_class = forms.OrganizationForm - template_name = 'organizations/organization_form.html' - success_url = reverse_lazy('organization_list') - success_message = "Organization updated successfully." - - -class OrganizationDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): - model = models.Organization - template_name = 'organizations/organization_confirm_delete.html' - success_url = reverse_lazy('organization_list') - success_message = "Organization deleted successfully." - - -class RepresentativeListView(LoginRequiredMixin, ListView): - model = models.Representative - template_name = 'representatives/representative_list.html' - context_object_name = 'representatives' - -class RepresentativeDetailView(DetailView): - model = models.Representative - template_name = 'representatives/representative_detail.html' - context_object_name = 'representative' - -class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin,CreateView): - model = models.Representative - form_class = forms.RepresentativeForm - template_name = 'representatives/representative_form.html' - success_url = reverse_lazy('representative_list') - success_message = "Representative created successfully." def form_valid(self, form): if form.is_valid(): - form.instance.dealer = self.request.user.dealer.get_parent_or_self + form.instance.dealer = self.request.user.dealer.get_root_dealer form.save() return super().form_valid(form) else: return form.errors -class RepresentativeUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): +class OrganizationUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + model = models.Organization + form_class = forms.OrganizationForm + template_name = "organizations/organization_form.html" + success_url = reverse_lazy("organization_list") + success_message = "Organization updated successfully." + + +class OrganizationDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): + model = models.Organization + template_name = "organizations/organization_confirm_delete.html" + success_url = reverse_lazy("organization_list") + success_message = "Organization deleted successfully." + + +class RepresentativeListView(LoginRequiredMixin, ListView): + model = models.Representative + template_name = "representatives/representative_list.html" + context_object_name = "representatives" + + +class RepresentativeDetailView(DetailView): + model = models.Representative + template_name = "representatives/representative_detail.html" + context_object_name = "representative" + + +class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = models.Representative form_class = forms.RepresentativeForm - template_name = 'representatives/representative_form.html' - success_url = reverse_lazy('representative_list') + template_name = "representatives/representative_form.html" + success_url = reverse_lazy("representative_list") + success_message = "Representative created successfully." + + def form_valid(self, form): + if form.is_valid(): + form.instance.dealer = self.request.user.dealer.get_root_dealer + form.save() + return super().form_valid(form) + else: + return form.errors + + +class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + model = models.Representative + form_class = forms.RepresentativeForm + template_name = "representatives/representative_form.html" + success_url = reverse_lazy("representative_list") success_message = "Representative updated successfully." -class RepresentativeDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): +class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): model = models.Representative - template_name = 'representatives/representative_confirm_delete.html' - success_url = reverse_lazy('representative_list') - success_message = "Representative deleted successfully." \ No newline at end of file + template_name = "representatives/representative_confirm_delete.html" + success_url = reverse_lazy("representative_list") + 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") diff --git a/quotation.pdf b/quotation.pdf new file mode 100644 index 00000000..e69de29b diff --git a/templates/base.html b/templates/base.html index 7ada0762..3ea0d6c6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,10 @@ + + + + + {% block extra_head %}{% endblock extra_head %} {% include 'header.html' %} @@ -102,7 +107,14 @@ small, .small { }); }); - + function save_as_pdf(){ + const quotationHtml = document.getElementById('quotation-html').outerHTML; + const printWindow = window.open('', '', 'height=500,width=800'); + printWindow.document.write(quotationHtml); + printWindow.document.close(); + printWindow.print(); + printWindow.close(); + } diff --git a/templates/sales/quotation_detail.html b/templates/sales/quotation_detail.html index ff05e3b7..d73566d7 100644 --- a/templates/sales/quotation_detail.html +++ b/templates/sales/quotation_detail.html @@ -31,9 +31,9 @@
-
+
-

{% trans "Quotation Details" %} - {{ quotation.id }}

+

{% trans "Quotation Details" %} - {{ quotation.display_quotation_number }}

@@ -47,7 +47,7 @@
{% trans "Quotation Information" %}
-

{% trans "Quotation No" %}: {{ quotation.id }}

+

{% trans "Quotation No" %}: {{ quotation.display_quotation_number }}

{% trans "Date" %}: {{ quotation.created_at|date }}

{% trans "Remarks" %}: {{ quotation.remarks }}

@@ -75,7 +75,7 @@ {{ item.quantity }} {{ item.car.finances.selling_price }} {{ 0.15 }} - {{ item.total }} + {{ item.total_vat}} {% endfor %} @@ -83,9 +83,9 @@ {% trans "Totals" %} {{ quotation.total_quantity }} - {{ total_sales_before_vat }} - {{ vat_amount }} - {{ total_sales_after_vat }} + {{ quotation.total }} + {{ vat_amount }} + {{ quotation.total_vat }} @@ -114,8 +114,7 @@ {{ total_cost }} {{ total_vat }} {{ total_cost_vat }} - - +
@@ -123,7 +122,15 @@ {% trans "Back to Quotations" %} {% if perms.inventory.change_carfinance and quotation.status == 'DRAFT' %} - {% endif %} + {% endif %} + Download as PDF + {% if not quotation.is_approved %} + Approve + {% endif %} + {% if quotation.is_approved %} + Generate Invoice + {% endif %} +
diff --git a/templates/sales/quotation_list.html b/templates/sales/quotation_list.html index 5968e347..c49714ea 100644 --- a/templates/sales/quotation_list.html +++ b/templates/sales/quotation_list.html @@ -11,6 +11,7 @@ # + {% trans "Quotation Number" %} {% trans "Customer" %} {% trans "Total Cars" %} {% trans "Total Amount" %} @@ -22,18 +23,15 @@ {% for quotation in quotations %} {{ forloop.counter }} + {{ quotation.quotation_number }} {{ quotation.customer.get_full_name }} {{ quotation.quotation_cars.count }} - {{ quotation.quotation_cars.get_financial_details.total_amount }} + {{ quotation.total_vat }} - {% if quotation.status == 'DRAFT' %} - {{ quotation.status }} - {% elif quotation.status == 'PENDING' %} - {{ quotation.status }} - {% elif quotation.status == 'CONFIRMED' %} - {{ quotation.status }} - {% elif quotation.status == 'CANCELED' %} - {{ quotation.status }} + {% if quotation.is_approved %} + Approved + {% else %} + Pending For Approval {% endif %} {{ quotation.created_at|date:"d/m/Y H:i" }} diff --git a/templates/sales/quotation_pdf.html b/templates/sales/quotation_pdf.html new file mode 100644 index 00000000..ffcd9af5 --- /dev/null +++ b/templates/sales/quotation_pdf.html @@ -0,0 +1,112 @@ +{% load static %} {% load i18n %} + +{% get_current_language as LANGUAGE_CODE %} + + + + + + {% block title %}{% trans 'HAIKAL' %}{% endblock %} + + + {% if LANGUAGE_CODE == 'ar' %} + + {% else %} + + {% endif %} + + + + +
+
+
+

{% trans "Quotation Details" %} - {{ quotation.quotation_number }}

+
+
+
+
+
{% trans "Customer Details" %}
+

+ {% trans "Name" %}: + {{ quotation.customer.get_full_name }}

+

{% trans "Address" %}: {{ quotation.customer.address }}

+

{% trans "VAT No" %}: {{ quotation.customer.vat_number }}

+
+
+
{% trans "Quotation Information" %}
+

{% trans "Quotation No" %}: {{ quotation.quotation_number }}

+

{% trans "Date" %}: {{ quotation.created_at|date }}

+

{% trans "Remarks" %}: {{ quotation.remarks }}

+
+
+ +
{% trans "Car Details" %}
+ + + + + + + + + + + + + + {% for item in quotation.quotation_cars.all %} + + + + + + + + + + {% endfor %} + + + + + + + + + + +
{% trans "VIN" %}{% trans "Model" %}{% trans "Year" %}{% trans "Quantity" %}{% trans "Price" %}{% trans "VAT" %}{% trans "Total" %}
{{ item.car.vin }}{{ item.car.id_car_model.get_local_name }}{{ item.car.year }}{{ item.quantity }}{{ item.car.finances.selling_price }}{{ 0.15 }}{{ item.total_vat}}
{% trans "Totals" %}{{ quotation.total_quantity }}{{ quotation.total }}{{ vat_amount }}{{ quotation.total_vat }}
+ +
{% trans "Additional Costs" %}
+ + + + + + + + + + + {% for service in services %} + + + + + + + {% endfor %} + + + + + + + +
{% trans "Additions" %}{% trans "Cost" %}{% trans "VAT %" %}{% trans "Total Cost with VAT" %}
{{service.name}}{{ service.price }}{{ service.vated }}{{ service.total_price_vat }}
{{ total_cost }}{{ total_vat }}{{ total_cost_vat }}
+
+
+
+ + \ No newline at end of file