diff --git a/inventory/admin.py b/inventory/admin.py index c11a3aa6..3e6f7337 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -25,6 +25,7 @@ admin.site.register(models.Organization) admin.site.register(models.Representative) admin.site.register(models.CarTrim) admin.site.register(models.AdditionalServices) +admin.site.register(models.Payment) @admin.register(models.CarMake) class CarMakeAdmin(admin.ModelAdmin): diff --git a/inventory/forms.py b/inventory/forms.py index f953956a..8123f86c 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -17,8 +17,9 @@ from .models import ( CarLocation, Organization, Representative, -SaleQuotationCar, -AdditionalServices + Payment, + SaleQuotationCar, + AdditionalServices ) from django.forms import ModelMultipleChoiceField @@ -28,6 +29,10 @@ from django.forms import formset_factory +class PaymentForm(forms.ModelForm): + class Meta: + model = Payment + fields = ['amount','payment_method', 'reference_number'] class UserForm(forms.ModelForm): class Meta: diff --git a/inventory/migrations/0014_payment_refund.py b/inventory/migrations/0014_payment_refund.py new file mode 100644 index 00000000..02c4ba1f --- /dev/null +++ b/inventory/migrations/0014_payment_refund.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.17 on 2024-12-24 13:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0013_salequotation_is_paid'), + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')), + ('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('SADAD', 'SADAD')], max_length=50, verbose_name='method')), + ('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')), + ('payment_date', models.DateField(auto_now_add=True, verbose_name='date')), + ('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='inventory.salequotation')), + ], + options={ + 'verbose_name': 'payment', + 'verbose_name_plural': 'payments', + }, + ), + migrations.CreateModel( + name='Refund', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')), + ('reason', models.TextField(blank=True, verbose_name='reason')), + ('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')), + ('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')), + ], + options={ + 'verbose_name': 'refund', + 'verbose_name_plural': 'refunds', + }, + ), + ] diff --git a/inventory/migrations/0015_alter_salequotation_payment_id.py b/inventory/migrations/0015_alter_salequotation_payment_id.py new file mode 100644 index 00000000..d863d075 --- /dev/null +++ b/inventory/migrations/0015_alter_salequotation_payment_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2024-12-24 14:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0014_payment_refund'), + ] + + operations = [ + migrations.AlterField( + model_name='salequotation', + name='payment_id', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Payment ID'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index a8366d25..3be8e819 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -675,7 +675,7 @@ class SaleQuotation(models.Model): updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) posted = models.BooleanField(default=False) - payment_id = models.CharField(max_length=10, null=True, blank=True, verbose_name=_("Payment ID")) + payment_id = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Payment ID")) is_paid = models.BooleanField(default=False) date_draft = models.DateTimeField(null=True, blank=True, verbose_name=_('Draft Date')) date_in_review = models.DateTimeField(null=True, blank=True, verbose_name=_('In Review Date')) @@ -838,7 +838,7 @@ class Payment(models.Model): verbose_name_plural = _("payments") def __str__(self): - return f"Payment of {self.amount} on {self.date} for {self.order}" + return f"Payment of {self.amount} on {self.payment_date} for {self.quotation}" class Refund(models.Model): diff --git a/inventory/signals.py b/inventory/signals.py index 77e62bca..6bc5e868 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -92,7 +92,8 @@ def create_ledger_entity(sender, instance, created, **kwargs): activate_accounts=True, coa_model=default_coa ) print(f"Ledger entity created for Dealer: {instance.name}") - + except Exception as e: + print(f"Failed to create Ledger entity for Dealer: {instance.name}. Error: {e}") # entity.create_account( # coa_model=coa, # code=1010, diff --git a/inventory/urls.py b/inventory/urls.py index fbc16ee6..c615e343 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -82,6 +82,10 @@ urlpatterns = [ path('sales/quotations//post_quotation/', views.post_quotation, name='post_quotation'), path('sales/quotations//invoice_detail/', views.invoice_detail, name='invoice_detail'), + #Payment URLs +# path('sales/quotations//payment/', views.PaymentCreateView.as_view(), name='payment_create'), + path('sales/quotations//payment/', views.payment_create, name='payment_create'), + # Users URLs path('user/create/', views.UserCreateView.as_view(), name='user_create'), path('user//update/', views.UserUpdateView.as_view(), name='user_update'), diff --git a/inventory/views.py b/inventory/views.py index ca36eeef..12e4e019 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -859,7 +859,7 @@ def generate_invoice(request, pk): origin="Payment", ) - quotation.payment_id = journal_entry.pk + quotation.payment_id = journal_entry.pk quotation.is_approved = True date = datetime.datetime.now() quotation.date_draft = date @@ -867,18 +867,18 @@ def generate_invoice(request, pk): invoice_model.save() quotation.save() - # invoice_model = invoice_model.filter(date_draft=quotation.date_draft).first() - # if not invoice_model.can_review(): - # messages.error(request, "Quotation is not ready for review") - # return redirect("quotation_detail", pk=pk) + + 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 - # qoutation.date_in_review = date - # qoutation.status = "In Review" - # invoice_model.save() - # qoutation.save() - # messages.success(request, _("Quotation Reviewed")) + 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": # messages.error(request, "Quotation is already approved") @@ -968,16 +968,16 @@ def post_quotation(request, pk): 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() + 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) - entity_unit,created = EntityUnitModel.objects.get_or_create( - name="Sales Department", - entity=entity, - document_prefix="SD" - ) + # entity_unit,created = EntityUnitModel.objects.get_or_create( + # name="Sales Department", + # entity=entity, + # document_prefix="SD" + # ) journal_entry = JournalEntryModel.objects.create( entity_unit=entity_unit, @@ -1017,58 +1017,44 @@ def mark_quotation(request, pk): entity = qoutation.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 == "in_review": - # if qoutation.status == "In Review": - # messages.error(request, "Quotation is already in review") - # return redirect("quotation_detail", pk=pk) + 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_draft=qoutation.date_draft).first() - # 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 - # qoutation.date_in_review = date - # qoutation.status = "In Review" - # invoice_model.save() - # qoutation.save() - # messages.success(request, _("Quotation Reviewed")) - # elif 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 = 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.get_root_dealer.user) + 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.mark_as_approved(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) - # invoice_model.date_approved = date - # qoutation.date_approved = date - # invoice_model.save() - # qoutation.status = "Approved" - # qoutation.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 = 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.get_root_dealer.user) - # invoice_model.date_paid = date - # qoutation.date_paid = date - # invoice_model.save() - # qoutation.status = "Paid" - # qoutation.save() - # messages.success(request, _("Quotation Paid")) + invoice_model.mark_as_paid(entity_slug=entity.slug, user_model=request.user.dealer.get_root_dealer.user) + 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) @@ -1389,4 +1375,73 @@ def payment_invoice(request,pk): invoice = invoice_model.filter(customer=customer,date_draft=quotation.date_draft).first() - return redirect('quotation_detail', pk=pk) \ No newline at end of file + 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 + + +def payment_create(request, pk): + quotation = get_object_or_404(models.SaleQuotation, pk=pk) + if request.method == "POST": + form = forms.PaymentForm(request.POST) + if form.is_valid(): + form.instance.quotation = quotation + insatnce = form.save() + + entity = quotation.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.get_root_dealer.user) + 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}) diff --git a/templates/sales/payments/confirm.html b/templates/sales/payments/confirm.html new file mode 100644 index 00000000..e69de29b diff --git a/templates/sales/payments/payment_create.html b/templates/sales/payments/payment_create.html new file mode 100644 index 00000000..f3587c5e --- /dev/null +++ b/templates/sales/payments/payment_create.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} +{% load i18n %} +{% block title %}{{ _("Payyment Create") }}{% endblock title %} +{% block content %} +
+
+
+
+
{{ _("Payment Create") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+{% endblock content %} + diff --git a/templates/sales/quotation_detail.html b/templates/sales/quotation_detail.html index 0be85aa8..b29fb650 100644 --- a/templates/sales/quotation_detail.html +++ b/templates/sales/quotation_detail.html @@ -148,13 +148,9 @@ {% elif quotation.status == 'Draft' and quotation.is_approved %} Mark As Review {% elif quotation.status == 'In Review' %} - Approve - {% elif quotation.status == 'Approved' %} - Mark As Paid - {% else %} - {% if not quotation.posted %} - Post - {% endif %} + Approve Quotation + {% elif quotation.status == 'Approved' %} + Pay {% endif %}