From d5d895789abde19098b3a99e1b055665e853693b Mon Sep 17 00:00:00 2001 From: Faheedkhan Date: Tue, 22 Jul 2025 12:00:49 +0300 Subject: [PATCH] added the logic for car_purchase and sale report --- inventory/models.py | 6 +- inventory/signals.py | 3 +- inventory/urls.py | 16 ++ inventory/views.py | 177 ++++++++++++++++-- templates/crm/notifications_history.html | 2 +- .../crm/opportunities/opportunity_form.html | 2 +- templates/dealers/dealer_form.html | 4 +- templates/footer.html | 12 +- templates/header.html | 22 +++ templates/ledger/reports/car_sale_report.html | 111 +++++++++++ templates/ledger/reports/purchase_report.html | 97 ++++++++++ templates/notifications.html | 8 +- .../plans/billing_info_create_or_update.html | 4 +- 13 files changed, 427 insertions(+), 37 deletions(-) create mode 100644 templates/ledger/reports/car_sale_report.html create mode 100644 templates/ledger/reports/purchase_report.html diff --git a/inventory/models.py b/inventory/models.py index 71ac33e9..01875154 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -639,6 +639,7 @@ class Car(Base): default=CarStatusChoices.AVAILABLE, verbose_name=_("Status"), ) + stock_type = models.CharField( max_length=10, choices=CarStockTypeChoices.choices, @@ -648,6 +649,7 @@ class Car(Base): remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) + sold_date=models.DateTimeField(verbose_name=_("Sold Date")) hash = models.CharField( max_length=64, blank=True, null=True, verbose_name=_("Hash") ) @@ -763,6 +765,7 @@ class Car(Base): "remarks": self.remarks, "mileage": self.mileage, "receiving_date": self.receiving_date.strftime("%Y-%m-%d %H:%M:%S"), + "sold_date":self.sold_date.strftime("%Y-%m-%d %H:%M:%S"), "hash": self.get_hash, "id": str(self.id), } @@ -901,7 +904,8 @@ class CarFinance(models.Model): verbose_name=_("Discount Amount"), default=Decimal("0.00"), ) - is_sold = models.BooleanField(default=False) + # is_sold = models.BooleanField(default=False) + @property def total(self): diff --git a/inventory/signals.py b/inventory/signals.py index 3eec0df6..dc311f32 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -292,7 +292,8 @@ def update_item_model_cost(sender, instance, created, **kwargs): :param kwargs: Additional keyword arguments passed during the signal invocation. :return: None """ - if created and not instance.is_sold: + # if created and not instance.is_sold: + if created: entity = instance.car.dealer.entity coa = entity.get_default_coa() inventory_account = ( diff --git a/inventory/urls.py b/inventory/urls.py index a3031cef..bf5cfb03 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -1107,6 +1107,22 @@ urlpatterns = [ views.PurchaseOrderMarkAsVoidView.as_view(), name="po-action-mark-as-void", ), + + # reports + path( + "/purchase-report/", + views.purchase_report_view, + name="po-report", + ), + path('purchase-report//csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'), + + path( + "/car-sale-report/", + views.car_sale_report_view, + name="car-sale-report", + ), + path('car-sale-report//csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'), + ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/views.py b/inventory/views.py index 3057f378..05999803 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1470,7 +1470,6 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi permission_required = ["inventory.add_carfinance"] def dispatch(self, request, *args, **kwargs): - print(self.car) self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return super().dispatch(request, *args, **kwargs) @@ -4233,22 +4232,16 @@ def sales_list_view(request, dealer_slug): :rtype: HttpResponse """ - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - entity = dealer.entity - print(entity) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) staff = getattr(request.user.staffmember, "staff", None) - print(staff) qs = [] try: if any([request.is_dealer, request.is_manager, request.is_accountant]): qs = models.ExtraInfo.get_sale_orders(staff=staff,is_dealer=True) - print(qs) elif request.is_staff: qs = models.ExtraInfo.get_sale_orders(staff=staff) - print(qs) except Exception as e: - print(e) - print(qs) + pass paginator = Paginator(qs, 30) @@ -4322,15 +4315,13 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): context = super().get_context_data(**kwargs) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) staff = getattr(self.request.user.staffmember, "staff", None) - if any([self.request.is_dealer ,self.request.is_manager ,self.request.is_accountant]): qs = models.ExtraInfo.objects.filter( dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(models.Staff), - ) - eqs=qs - print(qs) + ) + elif self.request.is_staff and self.request.is_sales: qs = models.ExtraInfo.objects.filter( dealer=dealer, @@ -4338,19 +4329,15 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): related_content_type=ContentType.objects.get_for_model(models.Staff), related_object_id=staff.pk, ) - - context["staff_estimates"] = qs + context["staff_estimates"] = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs]) return context def get_queryset(self): dealer = get_user_type(self.request) entity = dealer.entity status = self.request.GET.get("status") queryset = entity.get_estimates() - if status: queryset = queryset.filter(status=status) - - return queryset @@ -4462,7 +4449,7 @@ def create_estimate(request, dealer_slug, slug=None): for item in items_list: car_instance = models.Car.objects.filter( hash=item.get("item_id"), - finances__is_sold=False, + # finances__is_sold=False, colors__isnull=False, finances__isnull=False, finances__marked_price__gt=1, @@ -10444,3 +10431,155 @@ def bulk_update_car_price(request): class InventoryListView(InventoryListViewBase): template_name = "inventory/list.html" permission_required = ["django_ledger.view_purchaseordermodel"] + +@login_required +def purchase_report_view(request,dealer_slug): + pos = request.entity.get_purchase_orders() + data = [] + total_po_amount=0 + total_po_cars=0 + for po in pos: + items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] + + po_amount=0 + po_quantity=0 + for item in items: + po_amount+=item["total"] + po_quantity+=item["q"] + + total_po_amount+=po_amount + total_po_cars+=po_quantity + bills=po.get_po_bill_queryset() + vendors=set([bill.vendor.vendor_name for bill in bills]) + vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" + data.append({"po_number":po.po_number,"po_created":po.created,"po_status":po.po_status,"po_fulfilled_date":po.date_fulfilled,"po_amount":po_amount, + "po_quantity":po_quantity,"vendors_str":vendors_str}) + + current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") + context={ + "dealer":request.entity.name, + "time":current_time, + "data":data, + "total_po_amount":total_po_amount, + "total_po_cars":total_po_cars, + "current_time":current_time + + } + + + + return render(request,'ledger/reports/purchase_report.html',context) + + +def purchase_report_csv_export(request,dealer_slug): + response = HttpResponse(content_type='text/csv') + + + current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S") + filename = f"purchase_report_{dealer_slug}_{current_time}.csv" + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + writer = csv.writer(response) + + + header = [ + 'PO Number', + 'Created Date', + 'Status', + 'Fulfilled Date', + 'PO Amount', + 'PO Quantity', + 'Vendors' + ] + writer.writerow(header) + pos = request.entity.get_purchase_orders() + + for po in pos: + po_amount = 0 + po_quantity = 0 + items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] + + for item in items: + po_amount += item["total"] + po_quantity += item["q"] + + bills = po.get_po_bill_queryset() + vendors = set([bill.vendor.vendor_name for bill in bills ]) + vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" + + + writer.writerow([ + po.po_number, + po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', + po.get_po_status_display(), + po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '', + f"{po_amount:.2f}", + po_quantity, + vendors_str + ]) + return response + +@login_required +def car_sale_report_view(request,dealer_slug): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + cars_sold = models.Car.objects.filter(dealer=dealer,status='sold') + current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") + context={'cars_sold':cars_sold,'current_time':current_time } + return render(request,'ledger/reports/car_sale_report.html',context) + + +def car_sale_report_csv_export(request,dealer_slug): + + response = HttpResponse(content_type='text/csv') + + + current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") + filename = f"sales_report_{dealer_slug}_{current_time}.csv" + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + writer = csv.writer(response) + + header=[ + 'Make', + 'VIN', + 'Model', + 'Year', + 'Serie', + 'Trim', + 'Mileage', + 'Stock Type', + 'Created Date', + 'Sold Date', + 'Cost Price', + 'Marked Price', + 'Discount Amount', + 'Selling Price', + 'Tax Amount', + 'Invoice Number', + ] + writer.writerow(header) + + dealer=get_object_or_404(models.Dealer,slug=dealer_slug) + cars_sold=models.Car.objects.filter(dealer=dealer,status='sold') + for car in cars_sold: + writer.writerow([ + car.vin, + car.id_car_make.name, + car.id_car_model.name, + car.year, + car.id_car_serie.name, + car.id_car_trim.name, + car.mileage, + car.stock_type, + car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', + car.sold_date.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', + car.finances.cost_price, + car.finances.marked_price, + car.finances.discount_amount, + car.finances.selling_price, + car.finances.vat_amount, + car.item_model.invoicemodel_set.first().invoice_number + ]) + + return response + \ No newline at end of file diff --git a/templates/crm/notifications_history.html b/templates/crm/notifications_history.html index 50423916..fc307986 100644 --- a/templates/crm/notifications_history.html +++ b/templates/crm/notifications_history.html @@ -5,7 +5,7 @@

{{ _("Notifications") }}

{% if notifications %}
diff --git a/templates/crm/opportunities/opportunity_form.html b/templates/crm/opportunities/opportunity_form.html index 8f4ae6cb..c8ac7229 100644 --- a/templates/crm/opportunities/opportunity_form.html +++ b/templates/crm/opportunities/opportunity_form.html @@ -9,7 +9,7 @@ {% endif %} {% endblock %} {% block content %} -
+

diff --git a/templates/dealers/dealer_form.html b/templates/dealers/dealer_form.html index 5b11060a..426243ca 100644 --- a/templates/dealers/dealer_form.html +++ b/templates/dealers/dealer_form.html @@ -12,8 +12,8 @@
-

- {{ _("Update Dealer Information") }} +

+ {{ _("Update Dealer Information") }}

diff --git a/templates/footer.html b/templates/footer.html index d92a2b4e..1c0203ac 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -16,7 +16,7 @@
{% endcomment %} -
+{% comment %}
© 2025 {{ _("All right reserved")}} @@ -31,9 +31,9 @@
-
+
{% endcomment %} -{% comment %}
+ {% endcomment %} +
diff --git a/templates/header.html b/templates/header.html index 5ebbc1ae..567cd92c 100644 --- a/templates/header.html +++ b/templates/header.html @@ -346,6 +346,28 @@ {% trans 'Balance Sheet'|capfirst %}
+ + {% if request.user.is_authenticated %} + + {% else %} + + {% endif %} +
+ {% trans 'Car purchase Report'|capfirst %} +
+
+ + + {% if request.user.is_authenticated %} + + {% else %} + + {% endif %} +
+ {% trans 'Car Sale Report'|capfirst %} +
+
+
diff --git a/templates/ledger/reports/car_sale_report.html b/templates/ledger/reports/car_sale_report.html new file mode 100644 index 00000000..4bd3a76e --- /dev/null +++ b/templates/ledger/reports/car_sale_report.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% block title %} + {{ _("Car Sale Report") |capfirst }} +{% endblock title %} + +{% block content%} + +
+
+

Car Sale Report

+

{{dealer}}

+

Report Date: {{current_time}}

+
+ +
+
+

Report Summary

+
+
+
+
+
Total Revenue Amount
+

120000000

+
+
+
+
+
+
+
Total Vat Amount
+

12000

+
+
+
+
+
+
+
Total Discount Amount
+

12000

+
+
+
+ + + +
+ +
+ +
+

Detailed Purchase List

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + {% for car in cars_sold%} + + + + + + + + + + + + + + + + + + + {% endfor %} + +
VINMakeModelYearSerieTrimMileageStock TypeCreated DateSold DateCost PriceMarked PriceDiscount AmountSelling PriceTax AmountInvoice Number
{{car.vin}}{{car.id_car_make.name}}{{car.id_car_model.name}}{{car.year}}{{car.id_car_serie.name}}{{car.id_car_trim.name}}{{car.mileage}}{{car.stock_type}}{{car.created_at}}{{car.sold_date}}{{car.finances.cost_price}}{{car.finances.marked_price}}{{car.finances.discount_amount}}{{car.finances.selling_price}}{{car.finances.vat_amount}}{{car.item_model.invoicemodel_set.first.invoice_number}}
+
+
+
+
+ +{% endblock %} diff --git a/templates/ledger/reports/purchase_report.html b/templates/ledger/reports/purchase_report.html new file mode 100644 index 00000000..cf7770a5 --- /dev/null +++ b/templates/ledger/reports/purchase_report.html @@ -0,0 +1,97 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% block title %} + {{ _("Car Purchase Report") |capfirst }} +{% endblock title %} + + +{% block content%} + +
+
+

Car Purchase Report

+

{{dealer}}

+

Report Date: {{current_time}}

+
+ +
+
+

Report Summary

+
+
+
+
+
Total Purchase Amount
+

{{total_po_amount}}

+
+
+
+
+
+
+
Total Cars Purchased
+

{{total_po_cars}}

+
+
+
+ + + +
+ +
+ +
+

Detailed Purchase List

+ + + +
+ + + + + + + + + + + + + + + + {% for po in data %} + + + + + + + + + + + {% endfor %} + + + + + + + +
Purchase IDDate CreatedStatusPO AmountDate FulfilledCreated ByCars PurchasedVendor
{{po.po_number}}{{po.po_created}}{{po.po_status}}{{po.po_amount}}{{po.date_fulfilled}}staff{{po.po_quantity}} + {{po.vendors_str}} +
Total Purchase:{{total_po_amount}}
+
+
+
+
+ +{% endblock %} diff --git a/templates/notifications.html b/templates/notifications.html index 27b53ca0..c59f755f 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -32,7 +32,7 @@ -