diff --git a/inventory/models.py b/inventory/models.py index e867d83f..a148689c 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -413,11 +413,28 @@ class Car(models.Model): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" @property - def get_hash(self): + def get_hash(self): hash_object = hashlib.sha256() - hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}".encode('utf-8')) + color = "" + try: + color = self.colors.first().exterior.name if self.colors.exists() else "" + except: + pass + hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}{self.year}{self.id_car_serie.name}{self.id_car_trim.name}{color}".encode('utf-8')) return hash_object.hexdigest() + def mark_as_sold(self): + self.cancel_reservation() + self.status = CarStatusChoices.SOLD + self.save() + + def cancel_reservation(self): + if self.reservations.exists(): + self.reservations.all().delete() + def cancel_transfer(self): + if self.transfer_logs.exists(): + self.transfer_logs.all().delete() + def to_dict(self): return { "vin": self.vin, diff --git a/inventory/signals.py b/inventory/signals.py index e7704cb7..1dcfe2e4 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -723,6 +723,10 @@ def update_item_model_cost(sender, instance, created, **kwargs): # quotation.status = 'pending' # quotation.save() +@receiver(post_save, sender=models.CarColors) +def update_car_when_color_changed(sender, instance, **kwargs): + car = instance.car + car.save() @receiver(post_save, sender=models.Opportunity) def notify_staff_on_deal_stage_change(sender, instance, **kwargs): diff --git a/inventory/urls.py b/inventory/urls.py index 7d0c6755..f8ad51c2 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -181,6 +181,7 @@ urlpatterns = [ name="car_inventory", ), path("cars/inventory/stats", views.inventory_stats_view, name="inventory_stats"), + path("cars/inventory/list", views.CarListView.as_view(), name="car_list"), path("cars//", views.CarDetailView.as_view(), name="car_detail"), path("cars//update/", views.CarUpdateView.as_view(), name="car_update"), path("cars//delete/", views.CarDeleteView.as_view(), name="car_delete"), diff --git a/inventory/views.py b/inventory/views.py index 1f70ce10..8fb665d3 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -618,7 +618,7 @@ class CarColorCreate(LoginRequiredMixin, CreateView): def form_valid(self, form): car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) - form.instance.car = car + form.instance.car = car return super().form_valid(form) def get_success_url(self): @@ -629,6 +629,34 @@ class CarColorCreate(LoginRequiredMixin, CreateView): context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return context +class CarListView(LoginRequiredMixin, ListView): + model = models.Car + template_name = "inventory/car_list_view.html" + context_object_name = "cars" + paginate_by = 10 + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + cars = models.Car.objects.all() + + context["stats"] = { + 'all': cars.count(), + 'available':cars.filter(status='available').count(), + 'reserved':cars.filter(status='reserved').count(), + 'sold':cars.filter(status='sold').count(), + 'transfer':cars.filter(status='transfer').count() + } + return context + def get_queryset(self): + qs = super().get_queryset() + status = self.request.GET.get('status') + search = self.request.GET.get('search') + print(status) + if status: + qs=qs.filter(status=status) + if search: + query = Q(vin__icontains=search)|Q(id_car_make__name__icontains=search)|Q(id_car_model__name__icontains=search)|Q(id_car_trim__name__icontains=search)|Q(vin=search) + qs=qs.filter(query) + return qs @login_required def inventory_stats_view(request): @@ -2368,12 +2396,12 @@ def create_estimate(request): ) if isinstance(items, list): for item, quantity in zip(items, quantities): - if int(quantity) > models.Car.objects.filter(hash=item).count(): + if int(quantity) > models.Car.objects.filter(hash=item,status='available').count(): return JsonResponse( {"status": "error", "message": "Quantity must be less than or equal to the number of cars in stock"}, ) else: - if int(quantities) > models.Car.objects.filter(hash=item).count(): + if int(quantities) > models.Car.objects.filter(hash=items,status='available').count(): return JsonResponse( {"status": "error", "message": "Quantity must be less than or equal to the number of cars in stock"}, ) @@ -2441,7 +2469,7 @@ def create_estimate(request): if isinstance(items, list): for item in estimate_itemtxs.keys(): item_instance = ItemModel.objects.get(item_number=item) - instance = models.Car.objects.get(name=item_instance.name) + 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() @@ -2466,16 +2494,18 @@ def create_estimate(request): entity_slug=entity.slug, user_model=entity.admin ) form.fields["customer"].queryset = entity.get_customers().filter(active=True) - car_list = models.Car.objects.filter(dealer=dealer).exclude(status="reserved").values_list( - 'id_car_make__name', 'id_car_model__name','hash').distinct() - + car_list = models.Car.objects.filter(dealer=dealer).exclude(status="reserved").annotate(color=F('colors__exterior__name')).values_list( + 'id_car_make__name', 'id_car_model__name','id_car_serie__name','id_car_trim__name','color','hash').distinct() context = { "form": form, "items": [ { 'make':x[0], 'model':x[1], - 'hash': x[2] + 'serie':x[2], + 'trim':x[3], + 'color':x[4], + 'hash': x[5] } for x in car_list ], @@ -2513,7 +2543,16 @@ def create_sale_order(request, pk): if not estimate.is_approved(): estimate.mark_as_approved() estimate.save() - messages.success(request, "Sale Order created successfully") + for item in estimate.get_itemtxs_data()[0].all(): + try: + item.item_model.additional_info['car_info']['status'] = 'sold' + item.item_model.save() + except KeyError: + pass + + models.Car.objects.get(vin=item.item_model.name).mark_as_sold() + + messages.success(request, "Sale Order created successfully") return redirect("estimate_detail", pk=pk) form = forms.SaleOrderForm() @@ -2846,10 +2885,17 @@ def PaymentCreateView(request, pk): if not model.is_approved(): model.mark_as_approved(user_model=entity.admin) + if model.amount_paid == model.amount_due: + messages.error(request,"fully paid") + return redirect(redirect_url, pk=model.pk) + if model.amount_paid + amount > model.amount_due: + messages.error(request,"Amount exceeds due amount") + return redirect(redirect_url, pk=model.pk) + try: - if invoice: + if invoice: set_invoice_payment(dealer, entity, invoice, amount, payment_method) - elif bill: + elif bill: set_bill_payment(dealer, entity, bill, amount, payment_method) messages.success(request, "Payment created successfully!") return redirect(redirect_url, pk=model.pk) diff --git a/scripts/generate.py b/scripts/generate.py new file mode 100644 index 00000000..d235fb3e --- /dev/null +++ b/scripts/generate.py @@ -0,0 +1,59 @@ +from inventory.models import * +from rich import print +import random +import datetime + +from inventory.services import decodevin + + +def run(): + # car = Car.objects.filter(vin='2C3HD46R4WH170267') + vin_list = [ + "1B3ES56C13D120225", + "1GB4KYC86FF131536", + "1HSHXAHR15J136217", + "1G1ZT52845F231124", + "1J4GK48K43W721617", + "JTDBE32K430163717", + "1J4FA69S05P331572", + "2FMGK5D86EBD28496", + "KNADE243696530337", + "1N4AL21EX8N499928", + "1N4AL21E49N400571", + "1G2NW12E54C145398", + ] + for vin in vin_list: + try: + for _ in range(15): + dealer = Dealer.objects.get(user__email="ismail.mosa.ibrahim@gmail.com") + vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}" + result = decodevin(vin) + make = CarMake.objects.get(name=result["maker"]) + model = make.carmodel_set.filter(name__contains=result["model"]).first() + if not model or model == "": + model = random.choice(make.carmodel_set.all()) + year = result["modelYear"] + serie = random.choice(model.carserie_set.all()) + trim = random.choice(serie.cartrim_set.all()) + + car = Car.objects.create( + vin=vin, + id_car_make=make, + id_car_model=model, + id_car_serie=serie, + id_car_trim=trim, + year=(int(year) or 2025), + receiving_date=datetime.datetime.now(), + dealer=dealer, + ) + car_finance = CarFinance.objects.create( + car=car, cost_price=10000, selling_price=20000 + ) + car_color = CarColors.objects.create( + car=car, + interior=random.choice(InteriorColors.objects.all()), + exterior=random.choice(ExteriorColors.objects.all()), + ) + print(make, model, serie, trim) + except Exception as e: + print(e) diff --git a/templates/header.html b/templates/header.html index f89e2dc2..13eaefd0 100644 --- a/templates/header.html +++ b/templates/header.html @@ -74,6 +74,14 @@ + diff --git a/templates/inventory/car_list_view.html b/templates/inventory/car_list_view.html new file mode 100644 index 00000000..a6dea168 --- /dev/null +++ b/templates/inventory/car_list_view.html @@ -0,0 +1,187 @@ +{% extends "base.html" %} +{% load i18n %} +{% load custom_filters %} + +{% block customCSS %} + +{% endblock customCSS %} + +{% block content %} +
+
+ +
+
+
+
+
+ {% if page_obj.has_previous %} + + {% endif %} +
    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
+ {% if page_obj.has_next %} + + {% endif %} +
+
+ + + + + + + + + + + + + + + + {% for car in cars %} + + + + + + + + + + + + {% endfor %} + +
MakeModelYearTrimVINReceiving DateSTATUS
{{car.id_car_make}} +

{{car.id_car_model}}

+
+

{{car.year}}

+
+

{{car.id_car_trim}}

+
+

{{car.vin}}

+
+

{{car.receiving_date}}

+
+ {% if car.status == "available" %} + {{car.status}} + {% elif car.status == "reserved" %} + {{car.status}} + {% elif car.status == "sold" %} + {{car.status}} + {% elif car.status == "transfer" %} + {{car.status}} + {% endif %} + +
+ + +
+
+
+
+ +
+ {% if page_obj.has_previous %} + + {% endif %} +
    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
+ {% if page_obj.has_next %} + + {% endif %} +
+
+
+
+{% endblock %} + +{% block customJS %} + +{% endblock customJS %} \ No newline at end of file diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index a316fffc..b5a9a3e4 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -119,12 +119,13 @@ # - {% trans "Make" %} - {% trans "Model" %} - {% trans "Year" %} - {% trans "Quantity" %} - {% trans "Unit Price" %} - {% trans "Total" %} + {% trans "Make" %} + {% trans "Model" %} + {% trans "Year" %} + {% trans "VIN" %} + {% trans "Quantity" %} + {% trans "Unit Price" %} + {% trans "Total" %} @@ -135,25 +136,26 @@ {{item.make}} {{item.model}} {{item.year}} + {{item.vin}} {{item.quantity}} {{item.unit_price}} {{item.total}} {% endfor %} - {% trans "Vat" %} ({{data.vat}}%) + {% trans "Vat" %} ({{data.vat}}%) + {{data.total_vat_amount}} - {% trans "Discount Amount" %} + {% trans "Discount Amount" %} - {{data.total_discount}} - {% trans "Additional Services" %} + {% trans "Additional Services" %} {% for service in data.additionals %} + {{service.name}} - {{service.total}}
@@ -161,7 +163,7 @@ - {% trans "Grand Total" %} + {% trans "Grand Total" %} {{data.grand_total}} diff --git a/templates/sales/estimates/estimate_form.html b/templates/sales/estimates/estimate_form.html index c71daf7e..185ff353 100644 --- a/templates/sales/estimates/estimate_form.html +++ b/templates/sales/estimates/estimate_form.html @@ -15,10 +15,10 @@

{{ _("Cars") }}

-
+
@@ -68,7 +68,7 @@
diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index 46121451..7b29885f 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -212,12 +212,13 @@ # - {% trans "Make" %} - {% trans "Model" %} - {% trans "Year" %} - {% trans "Quantity" %} - {% trans "Unit Price" %} - {% trans "Total" %} + {% trans "Make" %} + {% trans "Model" %} + {% trans "Year" %} + {% trans "VIN" %} + {% trans "Quantity" %} + {% trans "Unit Price" %} + {% trans "Total" %} @@ -227,25 +228,26 @@ {{item.make}} {{item.model}} {{item.year}} + {{item.vin}} {{item.quantity}} {{item.total}} {{item.total}} {% endfor %} - {% trans "Discount Amount" %} + {% trans "Discount Amount" %} - {{data.total_discount}} - {% trans "VAT" %} ({{data.vat}}%) + {% trans "VAT" %} ({{data.vat}}%) + {{data.total_vat_amount}} - {% trans "Additional Services" %} + {% trans "Additional Services" %} {% for service in data.additionals %} + {{service.name}} - {{service.price}}
@@ -253,7 +255,7 @@ - {% trans "Grand Total" %} + {% trans "Grand Total" %} {{data.grand_total}} diff --git a/templates/sales/invoices/invoice_preview.html b/templates/sales/invoices/invoice_preview.html index 61157ad9..3bebe0fd 100644 --- a/templates/sales/invoices/invoice_preview.html +++ b/templates/sales/invoices/invoice_preview.html @@ -201,7 +201,10 @@ - + + + + @@ -211,6 +214,9 @@ {% for item in data.cars %} + + +
Item
الصنف
Make
الصنف
Model
الموديل
Year
السنة
VIN
رقم الهيكل
Quantity
العدد
Unit Price
سعر الوحدة
Total
الإجمالي
{{item.make}}{{item.model}}{{item.year}}{{item.vin}} {{item.quantity}} {{item.selling_price}} {{item.total}}