diff --git a/inventory/models.py b/inventory/models.py index c7a5e7c6..1a60b673 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -650,6 +650,7 @@ class Car(Base): default=CarStatusChoices.AVAILABLE, verbose_name=_("Status"), ) + stock_type = models.CharField( max_length=10, choices=CarStockTypeChoices.choices, @@ -659,6 +660,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") ) @@ -932,7 +934,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): @@ -3357,13 +3360,17 @@ class ExtraInfo(models.Model): related_object_id=staff.pk, ) # qs = qs.select_related("customer","estimate","invoice") - return [ - x.content_object.sale_orders.select_related( - "customer", "estimate", "invoice" - ).first() - for x in qs - if x.content_object.sale_orders.first() - ] + data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()]) + + return data + + # return [ + # x.content_object.sale_orders.select_related( + # "customer", "estimate", "invoice" + # ).first() + # for x in qs + # if x.content_object.sale_orders.first() + # ] @classmethod def get_invoices(cls, staff=None, is_dealer=False,dealer=None): diff --git a/inventory/signals.py b/inventory/signals.py index 54b4946e..3aef773d 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -294,7 +294,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 1f39b39f..152dbf61 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -1250,6 +1250,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/utils.py b/inventory/utils.py index 368cca6e..7aca476f 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -67,12 +67,15 @@ def get_jwt_token(): try: response = requests.post(url, headers=headers, json=data) response.raise_for_status() - # logging for success + #logging for success logger.info("Successfully fetched JWT token.") return response.text except requests.exceptions.RequestException as e: - # logging for error - logger.error(f"HTTP error fetching JWT token from {url}: ", exc_info=True) + #logging for error + logger.error( + f"HTTP error fetching JWT token from {url}: ", + exc_info=True + ) print(f"Error obtaining JWT token: {e}") return None @@ -160,7 +163,7 @@ def send_email(from_, to_, subject, message): message = message from_email = from_ recipient_list = [to_] - async_task(send_mail, subject, message, from_email, recipient_list) + async_task(send_mail,subject, message, from_email, recipient_list) def get_user_type(request): @@ -226,7 +229,7 @@ def reserve_car(car, request): ) car.status = models.CarStatusChoices.RESERVED car.save() - # --- Logging for Success --- + # --- Logging for Success --- logger.info( f"Car {car.pk} ('{car.id_car_make} {car.id_car_model}') reserved successfully " f"by user {request.user}. " @@ -240,7 +243,7 @@ def reserve_car(car, request): f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') " f"for user {request.user} . " f"Error: {e}", - exc_info=True, + exc_info=True ) messages.error(request, f"Error reserving car: {e}") @@ -1024,7 +1027,7 @@ class CarFinanceCalculator: def _get_vat_rate(self): - vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() + vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first() if not vat: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate @@ -1083,7 +1086,7 @@ class CarFinanceCalculator: total_additionals = sum( Decimal(item.price_) for item in self._get_additional_services()) - total_discount = self.extra_info.data.get("discount", 0) + total_discount = self.extra_info.data.get("discount",0) total_price_discounted = total_price if total_discount: @@ -1623,7 +1626,6 @@ def create_make_accounts(dealer): active=True, ) - def handle_payment(request, order): url = "https://api.moyasar.com/v1/payments" callback_url = request.build_absolute_uri( diff --git a/inventory/views.py b/inventory/views.py index 666c2011..bd48499d 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1494,7 +1494,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi form_class = forms.CarFinanceForm template_name = "inventory/car_finance_form.html" permission_required = ["inventory.add_carfinance"] - + def dispatch(self, request, *args, **kwargs): self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return super().dispatch(request, *args, **kwargs) @@ -1513,6 +1513,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["car"] = self.car + print(self.car) return context # def get_form(self, form_class=None): @@ -4319,8 +4320,8 @@ def sales_list_view(request, dealer_slug): item transactions specific to the user's entity. :rtype: HttpResponse """ - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - entity = dealer.entity + + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) staff = getattr(request.user.staffmember, "staff", None) qs = [] try: @@ -4448,16 +4449,19 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): entity = dealer.entity status = self.request.GET.get("status") queryset = entity.get_estimates() - + type(queryset) if status: queryset = queryset.filter(status=status) - # search_query = self.request.GET.get('q', '').strip() - # if search_query: - # queryset = queryset.filter( - # Q(quotation_number__icontains=search_query) | - # Q(customer__name__icontains=search_query) + for f in queryset.first()._meta.get_fields(): + print(f) + search_query = self.request.GET.get('q', '').strip() + print(search_query) + if search_query: + print("inside") + queryset = queryset.filter( + Q(estimate_number__icontains=search_query) - # ).distinct() # + ).distinct() return queryset @@ -4570,7 +4574,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, @@ -7233,7 +7237,7 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) return qs -class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): +class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ Represents a view for creating item expense entries. @@ -7258,6 +7262,7 @@ class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV form_class = ExpenseItemCreateForm template_name = "items/expenses/expense_create.html" success_url = reverse_lazy("item_expense_list") + success_message = "heeeeeeeee" permission_required = ["django_ledger.add_itemmodel"] def get_form_kwargs(self): @@ -7398,7 +7403,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context -class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): +class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") permission_required = "django_ledger.add_billmodel" @@ -7409,6 +7414,7 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVie } for_purchase_order = False for_estimate = False + success_message = "Bill Created" # Get user info for logging @@ -10393,7 +10399,7 @@ def upload_cars(request, dealer_slug, pk=None): form = forms.CSVUploadForm() form.fields["vendor"].queryset = dealer.vendors.all() - + print(request) return render( request, "csv_upload.html", @@ -10439,3 +10445,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/base.html b/templates/base.html index ebcf636b..b9dcc9f9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -38,7 +38,7 @@ - {% comment %} {% endcomment %} + {% comment %} {% endcomment %} @@ -81,8 +81,11 @@ {% include "plans/expiration_messages.html" %} {% block period_navigation %} {% endblock period_navigation %} - {% block content %} - {% endblock content %} +
+ {% block content %} + {% endblock content %} + +
{% block body %} {% endblock body %} {% include 'footer.html' %} diff --git a/templates/bill/bill_create.html b/templates/bill/bill_create.html index d46a325c..84f9f528 100644 --- a/templates/bill/bill_create.html +++ b/templates/bill/bill_create.html @@ -3,13 +3,24 @@ {% load static %} {% load django_ledger %} {% load crispy_forms_filters %} +{% block title %} + {{ _("Create Bill") |capfirst }} +{% endblock title %} + {% block content %} - {% comment %}
-
-
-
-

{% trans 'Create Bill' %}

-
+ + +
+ +
+
+
+

+ {% trans 'Create Bill' %} +

+
+
+
@@ -33,51 +44,19 @@ -
-
-
{% endcomment %} - -
-
-
-
-

{% trans 'Create Bill' %}

-
-
-
-
- {% csrf_token %} - {% if po_model %} -
-

{% trans 'Bill for' %} {{ po_model.po_number }}

-

{% trans 'Bill for' %} {{ po_model.po_title }}

-
- {% for itemtxs in po_itemtxs_qs %}{{ itemtxs }}{% endfor %} -
-
- {% endif %} -
{{ form|crispy }}
-
- -
-
+ + +
diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index b73a2a03..76eb1630 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -198,7 +198,7 @@ {% endif %}