diff --git a/inventory/migrations/0012_saleorder_created.py b/inventory/migrations/0012_saleorder_created.py new file mode 100644 index 00000000..e140e7ba --- /dev/null +++ b/inventory/migrations/0012_saleorder_created.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.17 on 2025-01-29 12:59 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0011_alter_saleorder_estimate'), + ] + + operations = [ + migrations.AddField( + model_name='saleorder', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/inventory/migrations/0013_alter_saleorder_options.py b/inventory/migrations/0013_alter_saleorder_options.py new file mode 100644 index 00000000..8aff2cc4 --- /dev/null +++ b/inventory/migrations/0013_alter_saleorder_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.17 on 2025-01-29 13:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0012_saleorder_created'), + ] + + operations = [ + migrations.AlterModelOptions( + name='saleorder', + options={'ordering': ['created']}, + ), + ] diff --git a/inventory/models.py b/inventory/models.py index be320ac2..832070e3 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1562,6 +1562,10 @@ class SaleOrder(models.Model): ]) comments = models.TextField(blank=True, null=True) formatted_order_id = models.CharField(max_length=10, unique=True, editable=False) + created = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created'] def save(self, *args, **kwargs): if not self.formatted_order_id: diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index fec36bb5..a60f2857 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -1,7 +1,7 @@ from django import template from calendar import month_abbr from django.urls import reverse -from django_ledger.io.io_core import get_localdate +from django_ledger.io.io_core import get_localdate,validate_activity register = template.Library() @@ -146,3 +146,35 @@ def balance_sheet_statement(context, io_model, to_date=None): 'user_model': user_model, 'tx_digest': io_digest.get_io_data(), } + +@register.inclusion_tag('django_ledger/financial_statements/tags/income_statement.html', takes_context=True) +def income_statement_table(context, io_model, from_date=None, to_date=None): + user_model = context['user'] + activity = context['request'].GET.get('activity') + activity = validate_activity(activity, raise_404=True) + entity_slug = context['view'].kwargs.get('entity_slug') + + if not from_date: + from_date = context['from_date'] + if not to_date: + to_date = context['to_date'] + + io_digest = io_model.digest( + activity=activity, + user_model=user_model, + entity_slug=entity_slug, + unit_slug=context['unit_slug'], + by_unit=context['by_unit'], + from_date=from_date, + to_date=to_date, + equity_only=True, + process_groups=True, + income_statement=True, + signs=True + ) + + return { + 'entity_slug': entity_slug, + 'user_model': user_model, + 'tx_digest': io_digest.get_io_data() + } diff --git a/inventory/urls.py b/inventory/urls.py index d7448a21..cf827d33 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -546,7 +546,23 @@ urlpatterns = [ path('entity//balance-sheet/date////', views.DateBalanceSheetView.as_view(), name='entity-bs-date'), - + # INCOME STATEMENT Reports ---- + # Entity ..... + path('entity//income-statement/', + views.BaseIncomeStatementRedirectViewBase.as_view(), + name='entity-ic'), + path('entity//income-statement/year//', + views.FiscalYearIncomeStatementViewBase.as_view(), + name='entity-ic-year'), + # path('entity//income-statement/quarter///', + # views.QuarterlyIncomeStatementView.as_view(), + # name='entity-ic-quarter'), + # path('entity//income-statement/month///', + # views.MonthlyIncomeStatementView.as_view(), + # name='entity-ic-month'), + # path('entity//income-statement/date////', + # views.MonthlyIncomeStatementView.as_view(), + # name='entity-ic-date'), ] diff --git a/inventory/utils.py b/inventory/utils.py index 72c92977..5eaef5f9 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ObjectDoesNotExist import json import random import datetime @@ -283,15 +284,11 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method): vat_amount = 0 total_amount = 0 - if invoice.terms == "on_receipt": - for x in invoice.get_itemtxs_data()[0].all(): - # vat_amount += models.Car.objects.get( - # vin=x.item_model.name - # ).finances.vat_amount * Decimal(x.quantity) - total_amount += Decimal(x.unit_cost) * Decimal(x.quantity) - - # grand_total = total_amount - Decimal(vat_amount) - total_amount + calculator = CarFinanceCalculator(invoice) + finance_data = calculator.get_finance_data() + # if invoice.terms == "on_receipt": + # for x in invoice.get_itemtxs_data()[0].all(): + # total_amount += Decimal(x.unit_cost) * Decimal(x.quantity) ledger = LedgerModel.objects.filter( name__icontains=str(invoice.pk), entity=entity @@ -322,28 +319,26 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method): TransactionModel.objects.create( journal_entry=journal, account=debit_account, # Debit Cash - amount=amount, # Payment amount + amount=finance_data["grand_total"], # Payment amount tx_type="debit", description="Payment Received", ) - # if total_amount + invoice. - TransactionModel.objects.create( journal_entry=journal, account=credit_account, # Credit Accounts Receivable - amount=total_amount, # Payment amount + amount=finance_data["total_price"], # Payment amount tx_type="credit", description="Payment Received", ) - if vat_amount > 0: - TransactionModel.objects.create( - journal_entry=journal, - account=vat_payable_account, # Credit VAT Payable - amount=vat_amount, - tx_type="credit", - description="VAT Payable on Invoice", + + TransactionModel.objects.create( + journal_entry=journal, + account=vat_payable_account, # Credit VAT Payable + amount=finance_data["total_vat_amount"], + tx_type="credit", + description="VAT Payable on Invoice", ) invoice.make_payment(amount) @@ -578,5 +573,99 @@ def to_dict(obj): obj_dict[key] = str(value) return obj_dict +class CarFinanceCalculator: + VAT_OBJ_NAME = 'vat_rate' + CAR_FINANCE_KEY = 'car_finance' + CAR_INFO_KEY = 'car_info' + ADDITIONAL_SERVICES_KEY = 'additional_services' + def __init__(self, model): + self.model = model + self.vat_rate = self._get_vat_rate() + self.item_transactions = self._get_item_transactions() + self.additional_services = self._get_additional_services() + def _get_vat_rate(self): + vat = models.VatRate.objects.filter(is_active=True).first() + if not vat: + raise ObjectDoesNotExist("No active VAT rate found") + return vat.rate + + def _get_item_transactions(self): + return self.model.get_itemtxs_data()[0].all() + + @staticmethod + def _get_quantity(item): + return item.ce_quantity or item.quantity + + def _get_nested_value(self, item, *keys): + current = item.item_model.additional_info + for key in keys: + current = current.get(key, {}) + return current + + def _get_car_data(self, item): + quantity = self._get_quantity(item) + car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) + car_info = self._get_nested_value(item, self.CAR_INFO_KEY) + unit_price = Decimal(car_finance.get('selling_price', 0)) + + return { + "item_number": item.item_model.item_number, + "vin": car_info.get('vin'), + "make": car_info.get('make'), + "model": car_info.get('model'), + "year": car_info.get('year'), + "trim": car_info.get('trim'), + "mileage": car_info.get('mileage'), + "cost_price": car_finance.get('cost_price'), + "selling_price": car_finance.get('selling_price'), + "discount": car_finance.get('discount_amount'), + "quantity": quantity, + "unit_price": unit_price, + "total": unit_price * Decimal(quantity), + "total_vat": car_finance.get('total_vat'), + "additional_services": self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY), + } + + def _get_additional_services(self): + return [ + {"name": service.name, "price": service.price} + for item in self.item_transactions + for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY) or [] + ] + + def calculate_totals(self): + total_price = sum( + Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, 'selling_price')) * + Decimal(self._get_quantity(item)) + for item in self.item_transactions + ) + + total_vat_amount = total_price * self.vat_rate + total_discount = sum( + Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, 'discount_amount')) + for item in self.item_transactions + ) + + return { + "total_price": total_price, + "total_vat_amount": total_vat_amount, + "total_discount": total_discount, + "grand_total": (total_price + total_vat_amount) - total_discount , + } + + def get_finance_data(self): + totals = self.calculate_totals() + + return { + "cars": [self._get_car_data(item) for item in self.item_transactions], + "quantity": sum(self._get_quantity(item) for item in self.item_transactions), + "total_price": totals['total_price'], + "total_vat": totals['total_vat_amount'] + totals['total_price'], + "total_vat_amount": totals['total_vat_amount'], + "total_discount": totals['total_discount'], + "grand_total": totals['grand_total'], + "additionals": self.additional_services, + "vat": self.vat_rate, + } \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index 27f57fcf..d7c9982b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,3 +1,4 @@ +from rich import print from decimal import Decimal from django.core.paginator import Paginator from django.forms import DateField, DateInput, HiddenInput, TextInput @@ -83,6 +84,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.models import Group from .utils import ( + CarFinanceCalculator, calculate_vat_amount, get_calculations, get_car_finance_data, @@ -1224,10 +1226,9 @@ class VendorCreateView( def form_valid(self, form): dealer = get_user_type(self.request) - # instance = form.save(commit=False) - form.instance.dealer = dealer - # instance.entity_model = dealer.entity - form.instance.save() + instance = form.save(commit=False) + instance.entity_model = dealer.entity + instance.save() return super().form_valid(form) @@ -1252,142 +1253,142 @@ def delete_vendor(request, pk): return redirect("vendor_list") -# class QuotationCreateView(LoginRequiredMixin, CreateView): -# model = models.SaleQuotation -# form_class = forms.QuotationForm -# template_name = "sales/quotation_form.html" -# -# def form_valid(self, form): -# form.instance.dealer = get_user_type(self.request) -# quotation = form.save() -# selected_cars = form.cleaned_data.get("cars") -# for car in selected_cars: -# car_finance = car.finances -# if car_finance: -# models.SaleQuotationCar.objects.create( -# quotation=quotation, -# car=car, -# ) -# -# messages.success(self.request, _("Quotation created successfully.")) -# return redirect("quotation_list") +class QuotationCreateView(LoginRequiredMixin, CreateView): + model = models.SaleQuotation + form_class = forms.QuotationForm + template_name = "sales/quotation_form.html" + + def form_valid(self, form): + form.instance.dealer = get_user_type(self.request) + quotation = form.save() + selected_cars = form.cleaned_data.get("cars") + for car in selected_cars: + car_finance = car.finances + if car_finance: + models.SaleQuotationCar.objects.create( + quotation=quotation, + car=car, + ) + + messages.success(self.request, _("Quotation created successfully.")) + return redirect("quotation_list") -# class QuotationListView(LoginRequiredMixin, ListView): -# model = models.SaleQuotation -# template_name = "sales/quotation_list.html" -# context_object_name = "quotations" -# paginate_by = 10 -# -# def get_queryset(self): -# status = self.request.GET.get("status") -# dealer = get_user_type(self.request) -# queryset = dealer.sales.all() -# if status: -# queryset = queryset.filter(status=status) -# return queryset +class QuotationListView(LoginRequiredMixin, ListView): + model = models.SaleQuotation + template_name = "sales/quotation_list.html" + context_object_name = "quotations" + paginate_by = 10 + + def get_queryset(self): + status = self.request.GET.get("status") + dealer = get_user_type(self.request) + queryset = dealer.sales.all() + if status: + queryset = queryset.filter(status=status) + return queryset -# class QuotationDetailView(LoginRequiredMixin, DetailView): -# model = models.SaleQuotation -# template_name = "sales/quotation_detail.html" -# context_object_name = "quotation" -# -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# quotation = self.object -# -# context_result = get_calculations(quotation) -# context.update(context_result) -# -# return context +class QuotationDetailView(LoginRequiredMixin, DetailView): + model = models.SaleQuotation + template_name = "sales/quotation_detail.html" + context_object_name = "quotation" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + quotation = self.object + + context_result = get_calculations(quotation) + context.update(context_result) + + return context -# @login_required -# def generate_invoice(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# entity = dealer.entity -# if not quotation.is_approved: -# messages.error( -# request, "Quotation must be approved before converting to an invoice." -# ) -# else: -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# -# invoice_model = entity.create_invoice( -# customer_model=customer, -# terms=InvoiceModel.TERMS_ON_RECEIPT, -# cash_account=cash_account.first(), -# prepaid_account=recivable_account.first(), -# coa_model=coa_qs.first(), -# ) -# -# name_list = [ -# f"{instance.car.year} {instance.car.id_car_make} {instance.car.id_car_model} {instance.car.id_car_trim}" -# for instance in quotation.quotation_cars.all() -# ] -# -# invoices_item_models = invoice_model.get_item_model_qs().filter( -# name__in=name_list -# ) -# -# invoice_itemtxs = { -# im.item_number: { -# "unit_cost": im.default_amount, -# "quantity": 1, -# "total_amount": im.default_amount, -# } -# for im in invoices_item_models -# } -# -# invoice_itemtxs = invoice_model.migrate_itemtxs( -# itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND -# ) -# ledger = ( -# entity.get_ledgers() -# .filter(name=f"Payment Ledger for Invoice {invoice_model}") -# .first() -# ) -# if not ledger: -# ledger = entity.create_ledger( -# name=f"Payment Ledger for Invoice {invoice_model}", posted=True -# ) -# journal_entry = JournalEntryModel.objects.create( -# posted=False, -# description=f"Payment for Invoice {invoice_model}", -# ledger=ledger, -# locked=False, -# origin="Payment", -# ) -# -# quotation.payment_id = journal_entry.pk -# quotation.is_approved = True -# date = datetime.datetime.now() -# quotation.date_draft = date -# invoice_model.date_draft = date -# invoice_model.save() -# quotation.save() -# -# if not invoice_model.can_review(): -# messages.error(request, "Quotation is not ready for review") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_review() -# invoice_model.date_in_review = date -# quotation.date_in_review = date -# quotation.status = "In Review" -# invoice_model.save() -# quotation.save() +@login_required +def generate_invoice(request, pk): + quotation = get_object_or_404(models.SaleQuotation, pk=pk) + dealer = get_user_type(request) + entity = dealer.entity + if not quotation.is_approved: + messages.error( + request, "Quotation must be approved before converting to an invoice." + ) + else: + coa_qs, coa_map = entity.get_all_coa_accounts() + cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") + recivable_account = ( + coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") + ) + customer = ( + entity.get_customers() + .filter(customer_name=quotation.customer.get_full_name) + .first() + ) + + invoice_model = entity.create_invoice( + customer_model=customer, + terms=InvoiceModel.TERMS_ON_RECEIPT, + cash_account=cash_account.first(), + prepaid_account=recivable_account.first(), + coa_model=coa_qs.first(), + ) + + name_list = [ + f"{instance.car.year} {instance.car.id_car_make} {instance.car.id_car_model} {instance.car.id_car_trim}" + for instance in quotation.quotation_cars.all() + ] + + invoices_item_models = invoice_model.get_item_model_qs().filter( + name__in=name_list + ) + + invoice_itemtxs = { + im.item_number: { + "unit_cost": im.default_amount, + "quantity": 1, + "total_amount": im.default_amount, + } + for im in invoices_item_models + } + + invoice_itemtxs = invoice_model.migrate_itemtxs( + itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND + ) + ledger = ( + entity.get_ledgers() + .filter(name=f"Payment Ledger for Invoice {invoice_model}") + .first() + ) + if not ledger: + ledger = entity.create_ledger( + name=f"Payment Ledger for Invoice {invoice_model}", posted=True + ) + journal_entry = JournalEntryModel.objects.create( + posted=False, + description=f"Payment for Invoice {invoice_model}", + ledger=ledger, + locked=False, + origin="Payment", + ) + + quotation.payment_id = journal_entry.pk + quotation.is_approved = True + date = datetime.datetime.now() + quotation.date_draft = date + invoice_model.date_draft = date + invoice_model.save() + quotation.save() + + if not invoice_model.can_review(): + messages.error(request, "Quotation is not ready for review") + return redirect("quotation_detail", pk=pk) + + invoice_model.mark_as_review() + invoice_model.date_in_review = date + quotation.date_in_review = date + quotation.status = "In Review" + invoice_model.save() + quotation.save() # elif status == "approved": # if qoutation.status == "Approved": @@ -1459,41 +1460,41 @@ def delete_vendor(request, pk): # invoice_model.mark_as_paid(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) # print("paid") # invoice_model.save() - # messages.success(request, "Invoice created") - # return redirect("quotation_detail", pk=pk) + 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 post_quotation(request, pk): -# qoutation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# entity = dealer.entity -# if qoutation.posted: -# messages.error(request, "Quotation is already posted") -# return redirect("quotation_detail", pk=pk) -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# customer = ( -# entity.get_customers() -# .filter(customer_name=qoutation.customer.get_full_name) -# .first() -# ) -# invoice_model = ( -# entity.get_invoices() -# .filter(customer=customer, date_paid=qoutation.date_paid) -# .first() -# ) -# ledger = ( -# entity.get_ledgers() -# .filter(name=f"Payment Ledger for Invoice {invoice_model}") -# .first() -# ) -# return +@login_required +def post_quotation(request, pk): + qoutation = get_object_or_404(models.SaleQuotation, pk=pk) + dealer = get_user_type(request) + entity = dealer.entity + if qoutation.posted: + messages.error(request, "Quotation is already posted") + return redirect("quotation_detail", pk=pk) + coa_qs, coa_map = entity.get_all_coa_accounts() + cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") + recivable_account = ( + coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") + ) + customer = ( + entity.get_customers() + .filter(customer_name=qoutation.customer.get_full_name) + .first() + ) + invoice_model = ( + entity.get_invoices() + .filter(customer=customer, date_paid=qoutation.date_paid) + .first() + ) + ledger = ( + entity.get_ledgers() + .filter(name=f"Payment Ledger for Invoice {invoice_model}") + .first() + ) + return # if not ledger: # ledger = entity.create_ledger(name=f"Payment Ledger for Invoice {invoice_model}",posted=True) @@ -1535,98 +1536,98 @@ def delete_vendor(request, pk): # return redirect("quotation_detail", pk=pk) -# @login_required -# def mark_quotation(request, pk): -# qoutation = get_object_or_404(models.SaleQuotation, pk=pk) -# status = request.GET.get("status") -# dealer = request.user.dealer -# entity = dealer.entity -# date = datetime.datetime.now() -# customer = ( -# entity.get_customers() -# .filter(customer_name=qoutation.customer.get_full_name) -# .first() -# ) -# invoice_model = entity.get_invoices().filter(customer=customer) -# if status == "approved": -# if qoutation.status == "Approved": -# messages.error(request, "Quotation is already approved") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model = invoice_model.filter( -# date_in_review=qoutation.date_in_review -# ).first() -# if not invoice_model.can_approve(): -# messages.error(request, "Quotation is not ready for approval") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_approved( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# invoice_model.date_approved = date -# qoutation.date_approved = date -# invoice_model.save() -# qoutation.status = "Approved" -# qoutation.save() -# for car in qoutation.quotation_cars.all(): -# car.car.status = "reserved" -# car.car.save() -# messages.success(request, _("Quotation Approved")) -# elif status == "paid": -# if qoutation.status == "Paid": -# messages.error(request, "Quotation is already paid") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model = invoice_model.filter( -# date_approved=qoutation.date_approved -# ).first() -# if not invoice_model.can_pay(): -# messages.error(request, "Quotation is not ready for payment") -# return redirect("quotation_detail", pk=pk) -# -# invoice_model.mark_as_paid( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# invoice_model.date_paid = date -# qoutation.date_paid = date -# invoice_model.save() -# qoutation.status = "Paid" -# qoutation.save() -# messages.success(request, _("Quotation Paid")) -# return redirect("quotation_detail", pk=pk) +@login_required +def mark_quotation(request, pk): + qoutation = get_object_or_404(models.SaleQuotation, pk=pk) + status = request.GET.get("status") + dealer = request.user.dealer + entity = dealer.entity + date = datetime.datetime.now() + customer = ( + entity.get_customers() + .filter(customer_name=qoutation.customer.get_full_name) + .first() + ) + invoice_model = entity.get_invoices().filter(customer=customer) + if status == "approved": + if qoutation.status == "Approved": + messages.error(request, "Quotation is already approved") + return redirect("quotation_detail", pk=pk) + + invoice_model = invoice_model.filter( + date_in_review=qoutation.date_in_review + ).first() + if not invoice_model.can_approve(): + messages.error(request, "Quotation is not ready for approval") + return redirect("quotation_detail", pk=pk) + + invoice_model.mark_as_approved( + entity_slug=entity.slug, user_model=request.user.dealer + ) + invoice_model.date_approved = date + qoutation.date_approved = date + invoice_model.save() + qoutation.status = "Approved" + qoutation.save() + for car in qoutation.quotation_cars.all(): + car.car.status = "reserved" + car.car.save() + messages.success(request, _("Quotation Approved")) + elif status == "paid": + if qoutation.status == "Paid": + messages.error(request, "Quotation is already paid") + return redirect("quotation_detail", pk=pk) + + invoice_model = invoice_model.filter( + date_approved=qoutation.date_approved + ).first() + if not invoice_model.can_pay(): + messages.error(request, "Quotation is not ready for payment") + return redirect("quotation_detail", pk=pk) + + invoice_model.mark_as_paid( + entity_slug=entity.slug, user_model=request.user.dealer + ) + invoice_model.date_paid = date + qoutation.date_paid = date + invoice_model.save() + qoutation.status = "Paid" + qoutation.save() + messages.success(request, _("Quotation Paid")) + return redirect("quotation_detail", pk=pk) -# @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 = quotation.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) -# -# models.SalesOrder.objects.create( -# quotation=quotation, -# 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) +@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 = quotation.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) + + models.SalesOrder.objects.create( + quotation=quotation, + 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, DetailView): -# model = models.SalesOrder -# template_name = "sales/sales_order_detail.html" -# context_object_name = "sales_order" -# slug_field = "order_id" -# slug_url_kwarg = "order_id" +class SalesOrderDetailView(LoginRequiredMixin, DetailView): + model = models.SalesOrder + template_name = "sales/sales_order_detail.html" + context_object_name = "sales_order" + slug_field = "order_id" + slug_url_kwarg = "order_id" # Users @@ -1874,64 +1875,64 @@ class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVi # return HttpResponse(pdf_file, content_type="application/pdf") -# @login_required -# def download_quotation_pdf(request, quotation_id): -# try: -# # Retrieve the quotation object -# quotation = models.SaleQuotation.objects.get(id=quotation_id) -# cars = models.SaleQuotationCar.objects.get(id=quotation_id) -# print(cars) -# services = cars.finance.additional_services.all() -# print(services) -# -# # Create a response object -# response = HttpResponse(content_type="application/pdf") -# response["Content-Disposition"] = ( -# f'attachment; filename="quotation_{quotation.id}.pdf"' -# ) -# -# # Call the PDF generation function -# # generate_quotation_pdf(response, quotation, services) -# -# return response -# except models.SaleQuotation.DoesNotExist: -# return HttpResponse("Quotation not found", status=404) +@login_required +def download_quotation_pdf(request, quotation_id): + try: + # Retrieve the quotation object + quotation = models.SaleQuotation.objects.get(id=quotation_id) + cars = models.SaleQuotationCar.objects.get(id=quotation_id) + print(cars) + services = cars.finance.additional_services.all() + print(services) + + # Create a response object + response = HttpResponse(content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="quotation_{quotation.id}.pdf"' + ) + + # Call the PDF generation function + # generate_quotation_pdf(response, quotation, services) + + return response + except models.SaleQuotation.DoesNotExist: + 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() -# -# invoice = invoice_model.filter( -# customer=customer, date_draft=quotation.date_draft -# ).first() -# return redirect("quotation_detail", pk=pk) +@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) -# @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) +@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): @@ -1952,75 +1953,75 @@ class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVi # return context -# def payment_create(request, pk): -# quotation = get_object_or_404(models.SaleQuotation, pk=pk) -# dealer = get_user_type(request) -# if request.method == "POST": -# form = forms.PaymentForm(request.POST) -# if form.is_valid(): -# form.instance.quotation = quotation -# insatnce = form.save() -# -# dealer = dealer -# entity = dealer.entity -# customer = ( -# entity.get_customers() -# .filter(customer_name=quotation.customer.get_full_name) -# .first() -# ) -# coa_qs, coa_map = entity.get_all_coa_accounts() -# cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") -# recivable_account = ( -# coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") -# ) -# journal_entry = JournalEntryModel.objects.filter( -# pk=quotation.payment_id -# ).first() -# TransactionModel.objects.create( -# journal_entry=journal_entry, -# account=cash_account.first(), # Debit Cash -# amount=insatnce.amount, # Payment amount -# tx_type="debit", -# description="Payment Received", -# ) -# -# TransactionModel.objects.create( -# journal_entry=journal_entry, -# account=recivable_account.first(), # Credit Accounts Receivable -# amount=insatnce.amount, # Payment amount -# tx_type="credit", -# description="Payment Received", -# ) -# journal_entry.posted = True -# quotation.posted = True -# quotation.save() -# journal_entry.save() -# -# invoice_model = ( -# entity.get_invoices() -# .filter(date_approved=quotation.date_approved) -# .first() -# ) -# -# invoice_model.mark_as_paid( -# entity_slug=entity.slug, user_model=request.user.dealer -# ) -# date = timezone.now() -# invoice_model.date_paid = date -# quotation.date_paid = date -# invoice_model.save() -# quotation.status = "Paid" -# quotation.save() -# -# messages.success(request, "Payment created successfully.") -# return redirect("quotation_detail", pk=pk) -# else: -# form = forms.PaymentForm() -# return render( -# request, -# "sales/payments/payment_create.html", -# {"quotation": quotation, "form": form}, -# ) +def payment_create(request, pk): + quotation = get_object_or_404(models.SaleQuotation, pk=pk) + dealer = get_user_type(request) + if request.method == "POST": + form = forms.PaymentForm(request.POST) + if form.is_valid(): + form.instance.quotation = quotation + insatnce = form.save() + + dealer = dealer + entity = dealer.entity + customer = ( + entity.get_customers() + .filter(customer_name=quotation.customer.get_full_name) + .first() + ) + coa_qs, coa_map = entity.get_all_coa_accounts() + cash_account = coa_qs.first().get_coa_accounts().filter(name="Cash") + recivable_account = ( + coa_qs.first().get_coa_accounts().filter(name="Accounts Receivable") + ) + journal_entry = JournalEntryModel.objects.filter( + pk=quotation.payment_id + ).first() + TransactionModel.objects.create( + journal_entry=journal_entry, + account=cash_account.first(), # Debit Cash + amount=insatnce.amount, # Payment amount + tx_type="debit", + description="Payment Received", + ) + + TransactionModel.objects.create( + journal_entry=journal_entry, + account=recivable_account.first(), # Credit Accounts Receivable + amount=insatnce.amount, # Payment amount + tx_type="credit", + description="Payment Received", + ) + journal_entry.posted = True + quotation.posted = True + quotation.save() + journal_entry.save() + + invoice_model = ( + entity.get_invoices() + .filter(date_approved=quotation.date_approved) + .first() + ) + + invoice_model.mark_as_paid( + entity_slug=entity.slug, user_model=request.user.dealer + ) + date = timezone.now() + invoice_model.date_paid = date + quotation.date_paid = date + invoice_model.save() + quotation.status = "Paid" + quotation.save() + + messages.success(request, "Payment created successfully.") + return redirect("quotation_detail", pk=pk) + else: + form = forms.PaymentForm() + return render( + request, + "sales/payments/payment_create.html", + {"quotation": quotation, "form": form}, + ) # Ledger @@ -2351,7 +2352,7 @@ def create_estimate(request): } ) - form = EstimateModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) + form = forms.EstimateModelCreateForm(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, finances__selling_price__gt=0 @@ -2379,15 +2380,11 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): estimate = kwargs.get("object") - if estimate.get_itemtxs_data(): - data = get_financial_values(estimate) - - kwargs["vat_amount"] = data["vat_amount"] - kwargs["total"] = data["grand_total"] - kwargs["discount_amount"] = data["discount_amount"] - kwargs["vat"] = data["vat"] - kwargs["car_and_item_info"] = data["car_and_item_info"] - kwargs["additional_services"] = data["additional_services"] + if estimate.get_itemtxs_data(): + calculator = CarFinanceCalculator(estimate) + finance_data = calculator.get_finance_data() + kwargs['data'] = finance_data + print(finance_data) kwargs["invoice"] = ( InvoiceModel.objects.all().filter(ce_model=estimate).first() ) @@ -2402,8 +2399,8 @@ def create_sale_order(request, pk): form = forms.SaleOrderForm(request.POST) if form.is_valid(): form.save() - if not estimate.is_completed(): - estimate.mark_as_completed() + if not estimate.is_approved(): + estimate.mark_as_approved() estimate.save() messages.success(request, "Sale Order created successfully") return redirect("estimate_detail", pk=pk) @@ -2411,11 +2408,13 @@ def create_sale_order(request, pk): form = forms.SaleOrderForm() form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) form.initial['estimate'] = estimate - data = get_car_finance_data(estimate) + # data = get_car_finance_data(estimate) + calculator = CarFinanceCalculator(estimate) + finance_data = calculator.get_finance_data() return render( request, "sales/estimates/sale_order_form.html", - {"form": form, "estimate": estimate, "items": items,"data": data}, + {"form": form, "estimate": estimate, "items": items,"data": finance_data}, ) def preview_sale_order(request,pk): @@ -2511,15 +2510,11 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): invoice = kwargs.get("object") - if invoice.get_itemtxs_data(): - data = get_financial_values(invoice) - - kwargs["vat_amount"] = data["vat_amount"] - kwargs["total"] = data["grand_total"] - kwargs["discount_amount"] = data["discount_amount"] - kwargs["vat"] = data["vat"] - kwargs["car_and_item_info"] = data["car_and_item_info"] - kwargs["additional_services"] = data["additional_services"] + if invoice.get_itemtxs_data(): + calculator = CarFinanceCalculator(invoice) + finance_data = calculator.get_finance_data() + print((finance_data["total_vat_amount"]+finance_data["total_price"]) == finance_data["grand_total"]) + kwargs["data"] = finance_data kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger ).all() @@ -2619,38 +2614,41 @@ def invoice_create(request, pk): ledger.save() invoice.save() - unit_items = estimate.get_itemtxs_data()[0] - vat = models.VatRate.objects.filter(is_active=True).first() - total = 0 - discount_amount = 0 + # unit_items = estimate.get_itemtxs_data()[0] + # vat = models.VatRate.objects.filter(is_active=True).first() + calculator = CarFinanceCalculator(estimate) + finance_data = calculator.get_finance_data() + + # total = 0 + # discount_amount = 0 - itemtxs = [] - for item in unit_items: - car = models.Car.objects.get(vin=item.item_model.name) + # itemtxs = [] + # for item in unit_items: + # car = models.Car.objects.get(vin=item.item_model.name) - total = Decimal(car.finances.total) * Decimal(item.ce_quantity) - discount_amount = car.finances.discount_amount + # total = Decimal(car.finances.total) * Decimal(item.ce_quantity) + # discount_amount = car.finances.discount_amount - grand_total = Decimal(total) - Decimal(discount_amount) - vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) - grand_total += Decimal(vat_amount) - unit_cost = grand_total / Decimal(item.ce_quantity) - itemtxs.append( - { - "item_number": item.item_model.item_number, - "unit_cost": unit_cost, - "unit_revenue": unit_cost, - "quantity": item.ce_quantity, - "total_amount": grand_total, - } - ) + # grand_total = Decimal(total) - Decimal(discount_amount) + # vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) + # grand_total += Decimal(vat_amount) + # unit_cost = grand_total / Decimal(item.ce_quantity) + # itemtxs.append( + # { + # "item_number": item.item_model.item_number, + # "unit_cost": unit_cost, + # "unit_revenue": unit_cost, + # "quantity": item.ce_quantity, + # "total_amount": grand_total, + # } + # ) invoice_itemtxs = { i.get("item_number"): { - "unit_cost": i.get("unit_cost"), + "unit_cost": i.get("total_vat"), "quantity": i.get("quantity"), - "total_amount": i.get("total_amount"), + "total_amount": i.get("total_vat"), } - for i in itemtxs + for i in finance_data.get("cars") } invoice_itemtxs = invoice.migrate_itemtxs( @@ -2697,14 +2695,16 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): invoice = kwargs.get("object") if invoice.get_itemtxs_data(): - data = get_financial_values(invoice) - - kwargs["vat_amount"] = data["vat_amount"] - kwargs["total"] = data["grand_total"] - kwargs["discount_amount"] = data["discount_amount"] - kwargs["vat"] = data["vat"] - kwargs["car_and_item_info"] = data["car_and_item_info"] - kwargs["additional_services"] = data["additional_services"] + # data = get_financial_values(invoice) + calculator = CarFinanceCalculator(invoice) + finance_data = calculator.get_finance_data() + kwargs["data"] = finance_data + # kwargs["vat_amount"] = data["vat_amount"] + # kwargs["total"] = data["grand_total"] + # kwargs["discount_amount"] = data["discount_amount"] + # kwargs["vat"] = data["vat"] + # kwargs["car_and_item_info"] = data["car_and_item_info"] + # kwargs["additional_services"] = data["additional_services"] return super().get_context_data(**kwargs) @@ -3425,7 +3425,8 @@ class SubscriptionPlans(ListView): class OrderListView(ListView): model = models.SaleOrder template_name = "sales/orders/order_list.html" - context_object_name = "orders" + context_object_name = "orders" + # email def send_email_view(request, pk): @@ -3501,7 +3502,7 @@ def custom_bad_request_view(request, exception=None): # from django_ledger.io.io_core import get_localdate # from django_ledger.views.mixins import (DjangoLedgerSecurityMixIn) # from django.views.generic import RedirectView -from django_ledger.views.financial_statement import FiscalYearBalanceSheetView +from django_ledger.views.financial_statement import FiscalYearBalanceSheetView,BaseIncomeStatementRedirectView,FiscalYearIncomeStatementView from django.views.generic import DetailView, RedirectView from django_ledger.io.io_core import get_localdate @@ -3540,3 +3541,34 @@ class DateBalanceSheetView(FiscalYearBalanceSheetViewBase, DateReportMixIn): """ Date Balance Sheet View. """ + +class BaseIncomeStatementRedirectViewBase(BaseIncomeStatementRedirectView): + + def get_redirect_url(self, *args, **kwargs): + year = get_localdate().year + dealer = get_user_type(self.request) + return reverse('entity-ic-year', + kwargs={ + 'entity_slug': dealer.entity.slug, + 'year': year + }) + +class FiscalYearIncomeStatementViewBase(FiscalYearIncomeStatementView): + template_name = "ledger/reports/income_statement.html" + +class QuarterlyIncomeStatementView(FiscalYearIncomeStatementView, QuarterlyReportMixIn): + """ + Quarter Income Statement View. + """ + + +class MonthlyIncomeStatementView(FiscalYearIncomeStatementView, MonthlyReportMixIn): + """ + Monthly Income Statement View. + """ + + +class DateModelIncomeStatementView(FiscalYearIncomeStatementView, DateReportMixIn): + """ + Date Income Statement View. + """ diff --git a/requirements.txt b/requirements.txt index 6f2f6dff..2f8a2658 100644 --- a/requirements.txt +++ b/requirements.txt @@ -184,6 +184,9 @@ python3-saml pytweening pytz pyvin +pywa +pywhat +pywhatkit PyYAML pyzbar qrcode diff --git a/scripts/run.py b/scripts/run.py index 5350fcd8..49d99a63 100644 --- a/scripts/run.py +++ b/scripts/run.py @@ -1,70 +1,45 @@ from decimal import Decimal -from django_ledger.models import EstimateModel +from django_ledger.models import EstimateModel,EntityModel from rich import print - +from datetime import date from inventory.models import VatRate +from inventory.utils import CarFinanceCalculator def run(): + # estimate = EstimateModel.objects.first() + # calculator = CarFinanceCalculator(estimate) + # finance_data = calculator.get_finance_data() + + # print(finance_data) + # entity = EntityModel.objects.get(name="ismail") + # bs_report = entity.get_balance_sheet_statement( + # to_date=date(2025, 1, 1), + # save_pdf=False, + # filepath='./' + # ) + + # ic_report = entity.get_income_statement( + # from_date=date(2022, 1, 1), + # to_date=date(2022, 12, 31), + # save_pdf=False, + # filepath='./' + # ) + + # # print(bs_report) + # print(ic_report.get_report_data()) estimate = EstimateModel.objects.first() - vat = VatRate.objects.filter(is_active=True).first() - data = estimate.get_itemtxs_data()[0].all() - total = sum( - [ - Decimal(item.item_model.additional_info["car_finance"]["selling_price"]) - * Decimal(item.ce_quantity or item.quantity) - for item in data - ] - ) - - additional_services = [] - - for i in data: - if i.item_model.additional_info["additional_services"]: - additional_services.extend( - [ - {"name": x.name, "price": x.price} - for x in i.item_model.additional_info["additional_services"] - ] - ) - cars_info = { - "cars": [ - { - "vin": x.item_model.additional_info["car_info"]["vin"], - "make": x.item_model.additional_info["car_info"]["make"], - "model": x.item_model.additional_info["car_info"]["model"], - "year": x.item_model.additional_info["car_info"]["year"], - "trim": x.item_model.additional_info["car_info"]["mileage"], - "cost_price": x.item_model.additional_info["car_finance"]["cost_price"], - "selling_price": x.item_model.additional_info["car_finance"][ - "selling_price" - ], - "discount": x.item_model.additional_info["car_finance"][ - "discount_amount" - ], - "total": x.item_model.additional_info["car_finance"]["total"], - "additional_services": x.item_model.additional_info[ - "additional_services" - ], - } - for x in data - ], - "quantity": data.count(), - "total_price": total, - "total__vat": (total * vat.rate) + total, - "total_discount": sum( - Decimal(x.item_model.additional_info["car_finance"]["discount_amount"]) - for x in data - ), - "grand_total": Decimal(total * vat.rate) - + total - - Decimal( - sum( - Decimal(x.item_model.additional_info["car_finance"]["discount_amount"]) - for x in data - ) - ), - "additionals": additional_services, - } - - print(cars_info) + calculator = CarFinanceCalculator(estimate) + finance_data = calculator.get_finance_data() + + + invoice_itemtxs = { + i.get("item_number"): { + "unit_cost": i.get("total_price"), + "quantity": i.get("quantity"), + "total_amount": i.get("total_vat"), + } + for i in finance_data.get("cars") + } + + print(finance_data) \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index 1d487538..f86811f1 100644 --- a/templates/header.html +++ b/templates/header.html @@ -235,14 +235,39 @@
{% trans 'bills'|capfirst %}
- - {% if request.user.dealer.entity.slug %} - + + + + + + @@ -143,7 +158,7 @@ {{car.year}}
- ${{car.total_vat}} + ${{car.total}}
diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index b17f5159..0405d962 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -237,7 +237,7 @@ {% trans "VAT" %} ({{vat}}%) - + {{vat_amount}} + + {{data.total_vat_amount}} diff --git a/templates/sales/invoices/invoice_preview.html b/templates/sales/invoices/invoice_preview.html index 55facc0a..62d538d7 100644 --- a/templates/sales/invoices/invoice_preview.html +++ b/templates/sales/invoices/invoice_preview.html @@ -208,11 +208,11 @@ - {% for item in car_and_item_info %} + {% for item in data.cars %} - {{item.info.make}} + {{item.make}} {{item.quantity}} - {{item.finances.selling_price}} + {{item.selling_price}} {{item.total}} {% endfor %} @@ -222,10 +222,10 @@
-

VAT/ضريبة القيمة المضافة ({{vat}}%): {{vat_amount}} {{ _("SAR") }}

+

VAT/ضريبة القيمة المضافة ({{vat}}%): {{data.vat}} {{ _("SAR") }}

Additional Services/ الخدمات الإضافية
- {% for service in additional_services %} + {% for service in data.additional_services %} {{service.name}} - {{service.price}} {{ _("SAR") }}
{% endfor %}

@@ -233,7 +233,7 @@
-

Total/الإجمالي {{total}} {{ _("SAR") }}

+

Total/الإجمالي {{data.grand_total}} {{ _("SAR") }}

diff --git a/templates/sales/orders/order_list.html b/templates/sales/orders/order_list.html index 98bf9d63..c6abd062 100644 --- a/templates/sales/orders/order_list.html +++ b/templates/sales/orders/order_list.html @@ -21,8 +21,11 @@ {{ order.formatted_order_id }} {{ order.estimate.customer.customer_name }} - {{ order.estimate }} - + + + {{ order.estimate }} + + {% comment %}