diff --git a/inventory/models.py b/inventory/models.py index c112ad6d..326a07a5 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -801,6 +801,7 @@ class Customer(models.Model): middle = f" {self.middle_name}" if self.middle_name else "" return f"{self.first_name}{middle} {self.last_name}" + @property def get_full_name(self): return f"{self.first_name} {self.middle_name} {self.last_name}" diff --git a/inventory/signals.py b/inventory/signals.py index 5914020c..e2f264c5 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -218,7 +218,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): active=True, ) #Create Deferred Revenue Account - entity.create_account( + deferred_revenue = entity.create_account( coa_model=coa, code="2060", role=roles.LIABILITY_CL_DEFERRED_REVENUE, @@ -226,6 +226,9 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="credit", active=True, ) + deferred_revenue.role_default = True + deferred_revenue.save() + # Create Vendor @receiver(post_save, sender=models.Vendor) diff --git a/inventory/urls.py b/inventory/urls.py index ef31d36b..ef5c9938 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -91,7 +91,7 @@ urlpatterns = [ path('generate_invoice//', views.generate_invoice, name='generate_invoice'), path('sales/quotations//mark_quotation/', views.mark_quotation, name='mark_quotation'), path('sales/quotations//post_quotation/', views.post_quotation, name='post_quotation'), - path('sales/quotations//invoice_detail/', views.invoice_detail, name='invoice_detail'), +# path('sales/quotations//invoice_detail/', views.invoice_detail, name='invoice_detail'), path('subscriptions', views.SubscriptionPlans.as_view(), name='subscriptions'), #Payment URLs # path('sales/quotations//payment/', views.PaymentCreateView.as_view(), name='payment_create'), @@ -140,6 +140,7 @@ urlpatterns = [ path('sales/estimates//send_email', views.send_email_view, name='send_email'), # Invoice path('sales/invoices/', views.InvoiceListView.as_view(), name='invoice_list'), + path('sales/invoices/create/', views.invoice_create, name='invoice_create'), path('sales/invoices//create/', views.invoice_create, name='invoice_create'), path('sales/invoices//', views.InvoiceDetailView.as_view(), name='invoice_detail'), path('sales/invoices//preview/', views.InvoicePreviewView.as_view(), name='invoice_preview'), diff --git a/inventory/views.py b/inventory/views.py index b5880da9..20a07783 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -749,7 +749,12 @@ class CustomerDetailView(LoginRequiredMixin, DetailView): model = models.Customer template_name = "customers/view_customer.html" context_object_name = "customer" - + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + name = f"{context['customer'].first_name} {context['customer'].middle_name} {context['customer'].last_name}" + context["estimates"] = self.request.entity.get_estimates().filter(customer__customer_name=name) + return context class CustomerCreateView( LoginRequiredMixin, @@ -1274,23 +1279,6 @@ def UserDeleteview(request, pk): return redirect("user_list") -# 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" @@ -1378,31 +1366,6 @@ class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVi success_message = "Representative deleted successfully." -# def quotation_pdf_view(request, pk): -# # Get the quotation object -# quotation = models.SaleQuotation.objects.get(pk=pk) -# -# # Render the HTML template for the quotation page -# context = { -# "quotation": quotation, -# } -# context_result = get_calculations(quotation) -# context = context.update(context_result) -# -# html_content = render_to_string("sales/quotation_pdf.html", context) -# -# # Create a PDF file -# -# pdf_file = HTML(string=html_content).render() -# -# # Save the PDF file to a file -# with open("quotation.pdf", "wb") as f: -# f.write(pdf_file.write_pdf()) -# -# # Return the PDF file as a response -# return HttpResponse(pdf_file, content_type="application/pdf") - - @login_required def download_quotation_pdf(request, quotation_id): try: @@ -1427,59 +1390,40 @@ def download_quotation_pdf(request, quotation_id): return HttpResponse("Quotation not found", status=404) -@login_required -def invoice_detail(request, pk): - quotation = get_object_or_404(models.SaleQuotation, pk=pk) - dealer = request.user.dealer - entity = dealer.entity - customer = ( - entity.get_customers() - .filter(customer_name=quotation.customer.get_full_name) - .first() - ) - invoice_model = entity.get_invoices() +# @login_required +# def invoice_detail(request, pk): +# quotation = get_object_or_404(models.SaleQuotation, pk=pk) +# dealer = request.user.dealer +# entity = dealer.entity +# customer = ( +# entity.get_customers() +# .filter(customer_name=quotation.customer.get_full_name) +# .first() +# ) +# invoice_model = entity.get_invoices() - invoice = invoice_model.filter( - customer=customer, date_draft=quotation.date_draft - ).first() - return redirect("quotation_detail", pk=pk) +# invoice = invoice_model.filter( +# customer=customer, date_draft=quotation.date_draft +# ).first() +# return redirect("quotation_detail", pk=pk) -@login_required -def payment_invoice(request, pk): - quotation = get_object_or_404(models.SaleQuotation, pk=pk) - dealer = request.user.dealer - entity = dealer.entity - customer = ( - entity.get_customers() - .filter(customer_name=quotation.customer.get_full_name) - .first() - ) - invoice_model = entity.get_invoices() - invoice = invoice_model.filter( - customer=customer, date_draft=quotation.date_draft - ).first() - - return redirect("quotation_detail", pk=pk) - - -# class PaymentCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): -# model = models.Payment -# form_class = forms.PaymentForm -# template_name = "sales/payments/payment_form.html" -# success_url = reverse_lazy("quotation_list") -# success_message = "Payment created successfully." - -# def form_valid(self, form): -# quotation = get_object_or_404(models.SaleQuotation, pk=self.kwargs["pk"]) -# form.instance.quotation = quotation -# form.save() -# return super().form_valid(form) -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context["quotation"] = get_object_or_404(models.SaleQuotation, pk=self.kwargs["pk"]) -# return context +# @login_required +# def payment_invoice(request, pk): +# quotation = get_object_or_404(models.SaleQuotation, pk=pk) +# dealer = request.user.dealer +# entity = dealer.entity +# customer = ( +# entity.get_customers() +# .filter(customer_name=quotation.customer.get_full_name) +# .first() +# ) +# invoice_model = entity.get_invoices() +# invoice = invoice_model.filter( +# customer=customer, date_draft=quotation.date_draft +# ).first() +# return redirect("quotation_detail", pk=pk) def payment_create(request, pk): quotation = get_object_or_404(models.SaleQuotation, pk=pk) @@ -1629,7 +1573,6 @@ def bank_account_delete(request, pk): # Accounts - class AccountListView(LoginRequiredMixin, ListView): model = AccountModel template_name = "ledger/coa_accounts/account_list.html" @@ -1705,36 +1648,6 @@ class EstimateListView(LoginRequiredMixin, ListView): return entity.get_estimates() -# class EstimateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): -# model = EstimateModel -# form_class = EstimateModelCreateForm -# template_name = "sales/estimates/estimate_form.html" -# success_url = reverse_lazy("estimate_list") -# success_message = "Estimate created successfully." - -# def get_form_kwargs(self): -# """ -# Override this method to pass additional keyword arguments to the form. -# """ -# entity = self.request.user.dealer.entity -# kwargs = super().get_form_kwargs() -# kwargs['entity_slug'] = entity.slug -# kwargs['user_model'] = entity.admin -# return kwargs - -# def get_context_data(self, **kwargs): -# entity = self.request.user.dealer.entity -# kwargs['items'] = entity.get_items_all() -# return super().get_context_data(**kwargs) -# def get_customer_queryset(self): -# entity = self.request.user.dealer.entity -# return entity.get_customer_queryset() - -# def form_valid(self, form): -# form.instance.entity = self.request.user.dealer.entity -# return super().form_valid(form) - - # @csrf_exempt @login_required def create_estimate(request): @@ -1831,12 +1744,12 @@ def create_estimate(request): for item in items: item_instance = ItemModel.objects.get(pk=item) instance = models.Car.objects.get(vin=item_instance.name) - response = reserve_car(instance, request) + reserve_car(instance, request) else: item_instance = ItemModel.objects.get(pk=items) instance = models.Car.objects.get(vin=item_instance.name) - response = reserve_car(instance, request) + reserve_car(instance, request) url = reverse("estimate_detail", kwargs={"pk": estimate.pk}) return JsonResponse( @@ -1846,16 +1759,7 @@ def create_estimate(request): "url": f"{url}", } ) - - # except Exception as e: - # return JsonResponse( - # { - # "status": "error", - # "message": f"An error occurred while processing the request: {str(e)}", - # }, - # status=400, - # ) - + form = EstimateModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) car_list = models.Car.objects.filter( dealer=dealer, finances__selling_price__gt=0 @@ -1894,13 +1798,10 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): ) vat = models.VatRate.objects.filter(is_active=True).first() - # Calculate VAT and total with 2 decimal places - vat_amount = round(total * vat.vat_rate, 2) # Round to 2 decimal places + vat_amount = round(total * vat.vat_rate, 2) grand_total = round( (total * vat.vat_rate) + total, 2 - ) # Round to 2 decimal places - - # Add values to the context + ) kwargs["vat_amount"] = vat_amount kwargs["total"] = grand_total kwargs["vat"] = vat.rate @@ -1945,7 +1846,6 @@ class EstimatePreviewView(LoginRequiredMixin, DetailView): @login_required def estimate_mark_as(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) - entity = estimate.entity mark = request.GET.get("mark") if mark: if mark == "review": @@ -1953,13 +1853,11 @@ def estimate_mark_as(request, pk): messages.error(request, "Estimate is not ready for review") return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_review() - elif mark == "approved": if not estimate.can_approve(): messages.error(request, "Estimate is not ready for approval") return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_approved() - messages.success(request, "Estimate approved successfully.") elif mark == "rejected": if not estimate.can_cancel(): @@ -1967,36 +1865,10 @@ def estimate_mark_as(request, pk): return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_canceled() messages.success(request, "Estimate canceled successfully.") - elif mark == "completed": if not estimate.can_complete(): messages.error(request, "Estimate is not ready for completion") - return redirect("estimate_detail", pk=estimate.pk) - - # invoice = entity.create_invoice(customer_model=estimate.customer, - # terms=estimate.terms, - # cash_account=entity.get_default_coa_accounts().get(name="Cash"), - # prepaid_account=entity.get_default_coa_accounts().get(name="Accounts Receivable"), - # coa_model=entity.get_default_coa() - # ) - - # unit_items = estimate.get_itemtxs_data()[0] - # invoice_itemtxs = { - # i.item_model.item_number: { - # 'unit_cost': i.ce_unit_cost_estimate, - # 'quantity': i.ce_quantity, - # 'total_amount': i.ce_cost_estimate - # } for i in unit_items - # } - - # invoice_itemtxs = invoice.migrate_itemtxs(itemtxs=invoice_itemtxs, - # commit=True, - # operation=InvoiceModel.ITEMIZE_APPEND) - # invoice.bind_estimate(estimate) - # invoice.mark_as_review() - # estimate.mark_as_completed() - # estimate.save() - # invoice.save() + return redirect("estimate_detail", pk=estimate.pk) estimate.save() messages.success(request, "Estimate marked as " + mark.upper()) @@ -2032,6 +1904,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): kwargs["vat_amount"] = total * vat.vat_rate kwargs["total"] = (total * vat.vat_rate) + total kwargs["vat"] = vat.rate + kwargs["amount_left"] = invoice.amount_due - invoice.amount_paid kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger ).all() @@ -2067,7 +1940,7 @@ def invoice_mark_as(request, pk): def invoice_create(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) - entity = request.user.dealer.entity + entity = request.entity form = InvoiceModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) if request.method == "POST": @@ -2081,6 +1954,7 @@ def invoice_create(request, pk): terms=invoice.terms, cash_account=invoice.cash_account, prepaid_account=invoice.prepaid_account, + payable_account=invoice.unearned_account, coa_model=entity.get_default_coa(), ) ledger = entity.create_ledger(name=f"Invoice {str(invoice_model.pk)}") @@ -2242,58 +2116,6 @@ class UserActivityLogListView(ListView): return queryset -# email -def send_email_view(request, pk): - estimate = get_object_or_404(EstimateModel, pk=pk) - if request.method == "POST": - # if not estimate.can_review(): - # messages.error(request, "Estimate is not ready for review") - # return redirect("estimate_detail", pk=estimate.pk) - if not estimate.get_itemtxs_data()[0]: - messages.error(request, "Estimate has no items") - return redirect("estimate_detail", pk=estimate.pk) - - send_email( - "manager@tenhal.com", - request.POST.get("to"), - request.POST.get("subject"), - request.POST.get("message"), - ) - # estimate.mark_as_review() - messages.success(request, "Email sent successfully!") - return redirect("estimate_detail", pk=estimate.pk) - link = reverse_lazy("estimate_preview", kwargs={"pk": estimate.pk}) - msg = f""" - السلام عليكم - Dear {estimate.customer.customer_name}, - - أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة. - - I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached. - - يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع. - - Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project. - - Estimate Link: - {link} - - شكراً لاهتمامكم بهذا الأمر. - Thank you for your attention to this matter. - - تحياتي, - Best regards, - [Your Name] - [Your Position] - [Your Company] - [Your Contact Information] - """ - return render( - request, - "sales/estimates/estimate_send.html", - {"estimate": estimate, "message": msg}, - ) - # CRM RELATED VIEWS def create_lead(request, pk): @@ -2467,3 +2289,72 @@ class SubscriptionPlans(ListView): model = models.SubscriptionPlan template_name = "subscriptions/subscription_plan.html" context_object_name = "plans" + + +# email +def send_email_view(request, pk): + estimate = get_object_or_404(EstimateModel, pk=pk) + if request.method == "POST": + # if not estimate.can_review(): + # messages.error(request, "Estimate is not ready for review") + # return redirect("estimate_detail", pk=estimate.pk) + if not estimate.get_itemtxs_data()[0]: + messages.error(request, "Estimate has no items") + return redirect("estimate_detail", pk=estimate.pk) + + send_email( + "manager@tenhal.com", + request.POST.get("to"), + request.POST.get("subject"), + request.POST.get("message"), + ) + # estimate.mark_as_review() + messages.success(request, "Email sent successfully!") + return redirect("estimate_detail", pk=estimate.pk) + link = reverse_lazy("estimate_preview", kwargs={"pk": estimate.pk}) + msg = f""" + السلام عليكم + Dear {estimate.customer.customer_name}, + + أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة. + + I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached. + + يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع. + + Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project. + + Estimate Link: + {link} + + شكراً لاهتمامكم بهذا الأمر. + Thank you for your attention to this matter. + + تحياتي, + Best regards, + [Your Name] + [Your Position] + [Your Company] + [Your Contact Information] + """ + return render( + request, + "sales/estimates/estimate_send.html", + {"estimate": estimate, "message": msg}, + ) + +# 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", {}) diff --git a/templates/customers/view_customer.html b/templates/customers/view_customer.html index 85c6ec04..ff974444 100644 --- a/templates/customers/view_customer.html +++ b/templates/customers/view_customer.html @@ -27,7 +27,7 @@ + href="{% url 'customer_delete' customer.pk %}"> {% trans 'Yes' %} @@ -37,8 +37,7 @@
- -
+

{% trans 'Customer details' %}

@@ -119,7 +118,7 @@
-

{{ _("Cars") }} (4)

+

{{ _("Cars") }} ({{ estimates.count }})

@@ -135,10 +134,21 @@ + {% for estimate in estimates %} - + - + @@ -151,55 +161,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% endfor %}
#2453#{{ estimate.estimate_number }} $87Paid + {% if estimate.is_draft %} + {% trans 'Draft' %} + {% elif estimate.is_review %} + {% trans 'Review' %} + {% elif estimate.is_approved %} + {% trans 'Approved' %} + {% else %} + {% trans 'Paid' %} + {% endif %} + Order Fulfilled Cash on delivery Dec 12, 12:56 PM
#2452$7264CancelledReady to pickupFree shippingDec 9, 2:28PM -
- - -
-
#2451$375PendingPartial FulfiLledLocal pickupDec 4, 12:56 PM -
- - -
-
#2450$657CancelledOrder CancelLedStandard shippingDec 1, 4:07 AM -
- - -
-
diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index 345a19c3..669f5e38 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -55,6 +55,7 @@

{% trans 'Paid Amount' %}

${{invoice.amount_paid}}

+
${{amount_left}} Left