From 2a397c3a2b2d6d92f49e0cb18edbe1512659f7d3 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 11 Jun 2025 15:07:00 +0300 Subject: [PATCH] add po --- inventory/forms.py | 31 +- ...edvehicle_purchaseorder_deliveryreceipt.py | 63 ++++ .../migrations/0016_purchaseorderitem_sale.py | 35 ++ .../0017_intendedvehicle_purchase_order.py | 20 ++ .../0018_intendedvehicle_quantity.py | 18 + ...019_alter_intendedvehicle_make_and_more.py | 33 ++ ...intendedvehicle_purchase_order_and_more.py | 68 ++++ inventory/models.py | 8 +- inventory/templatetags/custom_filters.py | 18 +- inventory/urls.py | 40 ++- inventory/views.py | 336 +++++++++++++++++- templates/bill/bill_create.html | 49 +++ templates/partials/vehicle_dropdowns.html | 135 +++++++ .../purchase_orders/includes/card_po.html | 237 ++++++++++++ .../includes/inventory_item_form.html | 21 ++ .../purchase_orders/includes/mark_as.html | 14 + .../includes/po_item_formset.html | 101 ++++++ .../purchase_orders/includes/po_table.html | 50 +++ .../purchase_orders/inventory_item_form.html | 53 +++ .../purchase_orders/partials/po-select.html | 13 + .../purchase_orders/po_confirm_delete.html | 19 + templates/purchase_orders/po_delete.html | 30 ++ templates/purchase_orders/po_detail.html | 100 ++++++ .../purchase_orders/po_detail_backup.html | 95 +++++ templates/purchase_orders/po_form.html | 53 +++ templates/purchase_orders/po_list.html | 74 ++++ templates/purchase_orders/po_update.html | 109 ++++++ .../purchase_orders/tags/po_item_table.html | 49 +++ 28 files changed, 1865 insertions(+), 7 deletions(-) create mode 100644 inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py create mode 100644 inventory/migrations/0016_purchaseorderitem_sale.py create mode 100644 inventory/migrations/0017_intendedvehicle_purchase_order.py create mode 100644 inventory/migrations/0018_intendedvehicle_quantity.py create mode 100644 inventory/migrations/0019_alter_intendedvehicle_make_and_more.py create mode 100644 inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py create mode 100644 templates/bill/bill_create.html create mode 100644 templates/partials/vehicle_dropdowns.html create mode 100644 templates/purchase_orders/includes/card_po.html create mode 100644 templates/purchase_orders/includes/inventory_item_form.html create mode 100644 templates/purchase_orders/includes/mark_as.html create mode 100644 templates/purchase_orders/includes/po_item_formset.html create mode 100644 templates/purchase_orders/includes/po_table.html create mode 100644 templates/purchase_orders/inventory_item_form.html create mode 100644 templates/purchase_orders/partials/po-select.html create mode 100644 templates/purchase_orders/po_confirm_delete.html create mode 100644 templates/purchase_orders/po_delete.html create mode 100644 templates/purchase_orders/po_detail.html create mode 100644 templates/purchase_orders/po_detail_backup.html create mode 100644 templates/purchase_orders/po_form.html create mode 100644 templates/purchase_orders/po_list.html create mode 100644 templates/purchase_orders/po_update.html create mode 100644 templates/purchase_orders/tags/po_item_table.html diff --git a/inventory/forms.py b/inventory/forms.py index d2bd4295..ac494e51 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -14,8 +14,9 @@ from .mixins import AddClassMixin from django_ledger.forms.invoice import ( InvoiceModelCreateForm as InvoiceModelCreateFormBase, ) - +from django.forms.models import inlineformset_factory from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase + from django_ledger.forms.journal_entry import ( JournalEntryModelCreateForm as JournalEntryModelCreateFormBase, ) @@ -42,6 +43,8 @@ from .models import ( Activity, Notes, CarModel, + CarSerie, + CarTrim, SaleOrder, CarMake, Customer, @@ -1899,3 +1902,29 @@ class StaffTaskForm(forms.ModelForm): widgets = { "due_date": forms.DateTimeInput(attrs={"type": "date"}), } + + +############################################################# + +class ItemInventoryForm(forms.Form): + make = forms.ModelChoiceField( + queryset=CarMake.objects.all(), + widget=forms.Select(attrs={"class": "form-control", "id": "make"}), + label=_("Make"), + ) + model = forms.ModelChoiceField( + queryset=CarModel.objects.none(), + widget=forms.Select(attrs={"class": "form-control", "id": "model"}), + label=_("Model"), + ) + serie = forms.ModelChoiceField( + queryset=CarSerie.objects.none(), + widget=forms.Select(attrs={"class": "form-control", "id": "serie"}), + label=_("Serie"), + ) + trim = forms.ModelChoiceField( + queryset=CarTrim.objects.none(), + widget=forms.Select(attrs={"class": "form-control", "id": "trim"}), + label=_("Trim"), + ) + diff --git a/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py b/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py new file mode 100644 index 00000000..26a7c129 --- /dev/null +++ b/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py @@ -0,0 +1,63 @@ +# Generated by Django 5.2.1 on 2025-06-03 11:03 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0014_alter_opportunity_amount'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='IntendedVehicle', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.PositiveIntegerField()), + ('color', models.CharField(max_length=30)), + ('engine', models.CharField(blank=True, max_length=50, null=True)), + ('condition', models.CharField(choices=[('new', 'New'), ('used', 'Used'), ('certified', 'Certified Pre-Owned')], max_length=20)), + ('expected_cost', models.DecimalField(decimal_places=2, max_digits=12)), + ('make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmake')), + ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmodel')), + ('serie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carserie')), + ('trim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.cartrim')), + ], + ), + migrations.CreateModel( + name='PurchaseOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('po_number', models.CharField(editable=False, max_length=50, unique=True)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='pending', max_length=20)), + ('quantity', models.PositiveIntegerField(default=1)), + ('total_cost', models.DecimalField(decimal_places=2, max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('expected_delivery_date', models.DateField(blank=True, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ('approved_at', models.DateTimeField(blank=True, null=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_orders', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_orders', to=settings.AUTH_USER_MODEL)), + ('intended_vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')), + ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.vendor')), + ], + ), + migrations.CreateModel( + name='DeliveryReceipt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('receipt_number', models.CharField(editable=False, max_length=50, unique=True)), + ('received_at', models.DateTimeField(default=django.utils.timezone.now)), + ('notes', models.TextField(blank=True, null=True)), + ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')), + ('received_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')), + ], + ), + ] diff --git a/inventory/migrations/0016_purchaseorderitem_sale.py b/inventory/migrations/0016_purchaseorderitem_sale.py new file mode 100644 index 00000000..295a2e43 --- /dev/null +++ b/inventory/migrations/0016_purchaseorderitem_sale.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.1 on 2025-06-03 11:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0015_intendedvehicle_purchaseorder_deliveryreceipt'), + ] + + operations = [ + migrations.CreateModel( + name='PurchaseOrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')), + ('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')), + ], + ), + migrations.CreateModel( + name='Sale', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sale_date', models.DateTimeField(auto_now_add=True)), + ('selling_price', models.DecimalField(decimal_places=2, max_digits=12)), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.customer')), + ('salesperson', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')), + ], + ), + ] diff --git a/inventory/migrations/0017_intendedvehicle_purchase_order.py b/inventory/migrations/0017_intendedvehicle_purchase_order.py new file mode 100644 index 00000000..feed14dc --- /dev/null +++ b/inventory/migrations/0017_intendedvehicle_purchase_order.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.1 on 2025-06-03 11:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0016_purchaseorderitem_sale'), + ] + + operations = [ + migrations.AddField( + model_name='intendedvehicle', + name='purchase_order', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='intended_vehicles', to='inventory.purchaseorder'), + preserve_default=False, + ), + ] diff --git a/inventory/migrations/0018_intendedvehicle_quantity.py b/inventory/migrations/0018_intendedvehicle_quantity.py new file mode 100644 index 00000000..d8336b71 --- /dev/null +++ b/inventory/migrations/0018_intendedvehicle_quantity.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-06-03 13:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0017_intendedvehicle_purchase_order'), + ] + + operations = [ + migrations.AddField( + model_name='intendedvehicle', + name='quantity', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py b/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py new file mode 100644 index 00000000..81564bb1 --- /dev/null +++ b/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.1 on 2025-06-03 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0018_intendedvehicle_quantity'), + ] + + operations = [ + migrations.AlterField( + model_name='intendedvehicle', + name='make', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='intendedvehicle', + name='model', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='intendedvehicle', + name='serie', + field=models.CharField(blank=True, max_length=30, null=True), + ), + migrations.AlterField( + model_name='intendedvehicle', + name='trim', + field=models.CharField(blank=True, max_length=30, null=True), + ), + ] diff --git a/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py b/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py new file mode 100644 index 00000000..bb76f0d8 --- /dev/null +++ b/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 5.2.1 on 2025-06-04 13:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0019_alter_intendedvehicle_make_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='intendedvehicle', + name='purchase_order', + ), + migrations.RemoveField( + model_name='purchaseorder', + name='intended_vehicle', + ), + migrations.RemoveField( + model_name='purchaseorderitem', + name='vehicle', + ), + migrations.RemoveField( + model_name='purchaseorder', + name='approved_by', + ), + migrations.RemoveField( + model_name='purchaseorder', + name='created_by', + ), + migrations.RemoveField( + model_name='purchaseorder', + name='supplier', + ), + migrations.RemoveField( + model_name='purchaseorderitem', + name='purchase_order', + ), + migrations.RemoveField( + model_name='sale', + name='car', + ), + migrations.RemoveField( + model_name='sale', + name='customer', + ), + migrations.RemoveField( + model_name='sale', + name='salesperson', + ), + migrations.DeleteModel( + name='DeliveryReceipt', + ), + migrations.DeleteModel( + name='IntendedVehicle', + ), + migrations.DeleteModel( + name='PurchaseOrder', + ), + migrations.DeleteModel( + name='PurchaseOrderItem', + ), + migrations.DeleteModel( + name='Sale', + ), + ] diff --git a/inventory/models.py b/inventory/models.py index f6ef07b0..3154f4b8 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -29,7 +29,7 @@ from django_ledger.models import ( EstimateModel, InvoiceModel, AccountModel, - EntityManagementModel, + EntityManagementModel ) from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -2855,3 +2855,9 @@ class PaymentHistory(models.Model): def is_successful(self): return self.status == self.COMPLETED + + + +###################################################################################################### +###################################################################################################### +###################################################################################################### diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 5d4283f1..c773c0d3 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -363,4 +363,20 @@ def currency_format(value): @register.filter def filter_by_role(accounts, role_prefix): - return [account for account in accounts if account.role.startswith(role_prefix)] \ No newline at end of file + return [account for account in accounts if account.role.startswith(role_prefix)] + +@register.inclusion_tag('purchase_orders/tags/po_item_table.html', takes_context=True) +def po_item_table1(context, queryset): + return { + 'entity_slug': context['entity_slug'], + 'po_model': context['po_model'], + 'po_item_list': queryset + } + +@register.inclusion_tag('purchase_orders/includes/po_item_formset.html', takes_context=True) +def po_item_formset_table(context, po_model, itemtxs_formset): + return { + 'entity_slug': context['view'].kwargs['entity_slug'], + 'po_model': po_model, + 'itemtxs_formset': itemtxs_formset, + } diff --git a/inventory/urls.py b/inventory/urls.py index a264c5aa..da5f3c65 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -693,7 +693,8 @@ path( ), # Bills path("items/bills/", views.BillListView.as_view(), name="bill_list"), - path("items/bills/create/", views.bill_create, name="bill_create"), + path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"), + # path("items/bills/create/", views.bill_create, name="bill_create"), path( "items/bills//bill_detail/", views.BillDetailView.as_view(), @@ -796,7 +797,7 @@ path( #dashboard api path('entity//data/net-payables/', views.PayableNetAPIView.as_view(), - name='entity-json-net-payables'), + name='entity-json-net-payables'), path('entity//data/net-receivables/', views.ReceivableNetAPIView.as_view(), name='entity-json-net-receivables'), @@ -808,8 +809,41 @@ path( path('management/user_management/', views.user_management, name='user_management'), path('management///activate_account/', views.activate_account, name='activate_account'), path('management///permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'), -] + ######### + # Purchase Order + path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'), + path('purchase_orders/new/', views.PurchaseOrderCreateView, name='purchase_order_create'), + path('purchase_orders//detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'), + path('purchase_orders///update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'), + path('purchase_orders//update//update-items/', + views.PurchaseOrderUpdateView.as_view(action_update_items=True), + name='purchase_order_update_items'), + path('purchase_orders/inventory_item//create/', views.InventoryItemCreateView, name='inventory_item_create'), + path('purchase_orders//inventory_items_filter/', views.inventory_items_filter, name='inventory_items_filter'), + path('purchase_orders//delete//', + views.PurchaseOrderModelDeleteView.as_view(), + name='po-delete'), + # Actions.... + path('/action//mark-as-draft/', + views.PurchaseOrderMarkAsDraftView.as_view(), + name='po-action-mark-as-draft'), + path('/action//mark-as-review/', + views.PurchaseOrderMarkAsReviewView.as_view(), + name='po-action-mark-as-review'), + path('/action//mark-as-approved/', + views.PurchaseOrderMarkAsApprovedView.as_view(), + name='po-action-mark-as-approved'), + path('/action//mark-as-fulfilled/', + views.PurchaseOrderMarkAsFulfilledView.as_view(), + name='po-action-mark-as-fulfilled'), + path('/action//mark-as-canceled/', + views.PurchaseOrderMarkAsCanceledView.as_view(), + name='po-action-mark-as-canceled'), + path('/action//mark-as-void/', + views.PurchaseOrderMarkAsVoidView.as_view(), + name='po-action-mark-as-void'), +] handler404 = "inventory.views.custom_page_not_found_view" handler500 = "inventory.views.custom_error_view" diff --git a/inventory/views.py b/inventory/views.py index 4142e952..bb4f2e5d 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5,7 +5,6 @@ import logging from datetime import datetime from time import sleep import numpy as np - # from rich import print from random import randint from decimal import Decimal @@ -20,6 +19,7 @@ from django.db import IntegrityError from background_task.models import Task from django.db.models.deletion import RestrictedError from django.http.response import StreamingHttpResponse +from django.core.exceptions import ImproperlyConfigured, ValidationError # Django from django.db.models import Q @@ -83,6 +83,10 @@ from django_ledger.forms.bank_account import ( BankAccountCreateForm, BankAccountUpdateForm, ) +from django_ledger.views.bill import ( + BillModelCreateView + # BillModelUpdateView as BillModelUpdateViewBase +) from django_ledger.forms.bill import ( ApprovedBillModelUpdateForm, InReviewBillModelUpdateForm, @@ -92,6 +96,19 @@ from django_ledger.forms.invoice import ( ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm, ) +from django_ledger.forms.item import ( + InventoryItemCreateForm, +) +from django_ledger.forms.purchase_order import (PurchaseOrderModelCreateForm, BasePurchaseOrderModelUpdateForm, + DraftPurchaseOrderModelUpdateForm, ReviewPurchaseOrderModelUpdateForm, + ApprovedPurchaseOrderModelUpdateForm, + get_po_itemtxs_formset_class) +from django_ledger.views.purchase_order import ( + PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase, + PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, + BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, + PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase +) from django_ledger.models import ( ItemTransactionModel, EntityModel, @@ -105,6 +122,7 @@ from django_ledger.models import ( ItemModel, BillModel, LedgerModel, + PurchaseOrderModel ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -6263,6 +6281,7 @@ def bill_mark_as_paid(request, pk): return redirect("bill_detail", pk=bill.pk) + @login_required @permission_required("django_ledger.add_billmodel", raise_exception=True) def bill_create(request): @@ -8275,3 +8294,318 @@ def permenant_delete_account(request, content_type, slug): return render( request, "admin_management/permenant_delete_account.html", {"obj": obj} ) + + +##################################################################### + + +def PurchaseOrderCreateView(request): + dealer = get_user_type(request) + entity = dealer.entity + if(request.method == "POST"): + po = entity.create_purchase_order(po_title=request.POST.get("po_title")) + po.entity = entity + po.save() + messages.success(request, _("Purchase order created successfully")) + return redirect('purchase_order_detail', pk=po.pk) + + form = PurchaseOrderModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) + return render(request, "purchase_orders/po_form.html", {"form": form}) + +def InventoryItemCreateView(request,pk): + po = get_object_or_404(PurchaseOrderModel, pk=pk) + dealer = get_user_type(request) + entity = dealer.entity + coa = entity.get_default_coa() + inventory_accounts = entity.get_coa_all().get(name='ASSET_CA_INVENTORY') + if(request.method == "POST"): + make = request.POST.get("make") + model = request.POST.get("model") + serie = request.POST.get("serie") + trim = request.POST.get("trim") + + make_name = models.CarMake.objects.get(pk=make) + model_name = models.CarModel.objects.get(pk=model) + serie_name = models.CarSerie.objects.get(pk=serie) + trim_name = models.CarTrim.objects.get(pk=trim) + + inventory_name = f"{make_name.name} - {model_name.name} - {serie_name.name} - {trim_name.name}" + uom = entity.get_uom_all().get(name='Unit') + entity.create_item_inventory( + name=inventory_name, + uom_model=uom, + item_type=ItemModel.ITEM_TYPE_MATERIAL + ) + messages.success(request, _("Inventory item created successfully")) + return redirect('purchase_order_detail', pk=po.pk) + + +class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase): + template_name = 'purchase_orders/po_detail.html' + context_object_name = 'po_model' + + + def get_queryset(self): + dealer = get_user_type(self.request) + self.queryset = PurchaseOrderModel.objects.for_entity( + entity_slug=dealer.entity.slug, + user_model=dealer.entity.admin + ).select_related('entity', 'ce_model') + return super().get_queryset() + + def get_context_data(self, **kwargs): + dealer = get_user_type(self.request) + context = super().get_context_data(**kwargs) + context['entity_slug'] = dealer.entity.slug + return context + + +def inventory_items_filter(request,po_pk): + dealer = get_user_type(request) + make = request.GET.get('make') + model = request.GET.get('model') + serie = request.GET.get('serie') + make_data = models.CarMake.objects.all() + model_data = models.CarModel.objects.none() + serie_data = models.CarSerie.objects.none() + trim_data = models.CarTrim.objects.none() + if make: + make = models.CarMake.objects.get(pk=make) + model_data = make.carmodel_set.all() + elif model: + model = models.CarModel.objects.get(pk=model) + serie_data = model.carserie_set.all() + elif serie: + serie = models.CarSerie.objects.get(pk=serie) + trim_data = serie.cartrim_set.all() + context = { + 'make_data': make_data, + 'model_data': model_data, + 'serie_data': serie_data, + 'trim_data': trim_data, + 'inventory_accounts': dealer.entity.get_coa_accounts().filter(role="asset_ca_inv"), + 'inventory_items': dealer.entity.get_items_inventory(), + 'entity_slug': dealer.entity.slug, + 'po_model': get_object_or_404(PurchaseOrderModel, pk=po_pk) + } + return render(request, "purchase_orders/po_detail.html", context) + + +# def PurchaseOrderDetailView(request, pk): +# po = get_object_or_404(PurchaseOrderModel, pk=pk) +# dealer = get_user_type(request) + +# make = request.GET.get('make') +# model = request.GET.get('model') +# serie = request.GET.get('serie') +# make_data = models.CarMake.objects.all() +# model_data = models.CarModel.objects.none() +# serie_data = models.CarSerie.objects.none() +# trim_data = models.CarTrim.objects.none() +# if make: +# make = models.CarMake.objects.get(pk=make) +# model_data = make.carmodel_set.all() +# elif model: +# model = models.CarModel.objects.get(pk=model) +# serie_data = model.carserie_set.all() +# elif serie: +# serie = models.CarSerie.objects.get(pk=serie) +# trim_data = serie.cartrim_set.all() +# context = { +# 'make_data': make_data, +# 'model_data': model_data, +# 'serie_data': serie_data, +# 'trim_data': trim_data, +# 'po': po, +# 'inventory_accounts': dealer.entity.get_coa_accounts().filter(role="asset_ca_inv"), +# 'inventory_items': dealer.entity.get_items_inventory(), +# } +# return render(request, "purchase_orders/po_detail.html", context) + + +class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): + model = PurchaseOrderModel + context_object_name = "purchase_orders" + paginate_by = 30 + template_name = "purchase_orders/po_list.html" + permission_required = ["inventory.view_carfinance"] + + def get_queryset(self): + dealer = get_user_type(self.request) + entity = dealer.entity + return self.model.objects.filter(entity=entity) + + def get_context_data(self, **kwargs): + dealer = get_user_type(self.request) + context = super().get_context_data(**kwargs) + context['entity_slug'] = dealer.entity.slug + return context + +class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): + template_name = 'purchase_orders/po_update.html' + context_object_name = 'po_model' + def get_context_data(self, **kwargs): + dealer = get_user_type(self.request) + context = super().get_context_data(**kwargs) + context['entity_slug'] = dealer.entity.slug + context['make_data'] = models.CarMake.objects.all() + context['model_data'] = models.CarModel.objects.none() + context['serie_data'] = models.CarSerie.objects.none() + context['trim_data'] = models.CarTrim.objects.none() + return context + + def get_success_url(self): + return reverse('purchase_order_update', + kwargs={ + 'entity_slug': self.kwargs['entity_slug'], + 'po_pk': self.kwargs['po_pk'] + }) + def get(self, request, entity_slug, po_pk, *args, **kwargs): + if self.action_update_items: + return HttpResponseRedirect( + redirect_to=reverse('purchase_order_update', + kwargs={ + 'entity_slug': entity_slug, + 'po_pk': po_pk + }) + ) + return super(PurchaseOrderModelUpdateViewBase, self).get(request, entity_slug, po_pk, *args, **kwargs) + + def post(self, request, entity_slug, *args, **kwargs): + dealer = get_user_type(self.request) + if self.action_update_items: + + if not request.user.is_authenticated: + return HttpResponseForbidden() + + queryset = self.get_queryset() + po_model: PurchaseOrderModel = self.get_object(queryset=queryset) + self.object = po_model + po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) + itemtxs_formset = po_itemtxs_formset_class(request.POST, + user_model=dealer.entity.admin, + po_model=po_model, + entity_slug=entity_slug) + + if itemtxs_formset.has_changed(): + if itemtxs_formset.is_valid(): + itemtxs_list = itemtxs_formset.save(commit=False) + create_bill_uuids = [ + str(i['uuid'].uuid) for i in itemtxs_formset.cleaned_data if i and i['create_bill'] is True + ] + + if create_bill_uuids: + item_uuids = ','.join(create_bill_uuids) + redirect_url = reverse( + 'django_ledger:bill-create-po', + kwargs={ + 'entity_slug': self.kwargs['entity_slug'], + 'po_pk': po_model.uuid, + } + ) + redirect_url += f'?item_uuids={item_uuids}' + return HttpResponseRedirect(redirect_url) + + for itemtxs in itemtxs_list: + if not itemtxs.po_model_id: + itemtxs.po_model_id = po_model.uuid + itemtxs.clean() + + itemtxs_list = itemtxs_formset.save() + po_model.update_state() + po_model.clean() + po_model.save(update_fields=['po_amount', + 'po_amount_received', + 'updated']) + # if valid get saved formset from DB + messages.add_message(request, messages.SUCCESS, 'PO items updated successfully.') + return self.render_to_response(context=self.get_context_data()) + # if not valid, return formset with errors... + return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset)) + return super(PurchaseOrderUpdateView, self).post(request,entity_slug, *args, **kwargs) + + def get_form(self, form_class=None): + po_model: PurchaseOrderModel = self.object + if po_model.is_draft(): + return DraftPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user, + **self.get_form_kwargs() + ) + elif po_model.is_review(): + return ReviewPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user, + **self.get_form_kwargs() + ) + elif po_model.is_approved(): + return ApprovedPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user, + **self.get_form_kwargs() + ) + return BasePurchaseOrderModelUpdateForm( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user, + **self.get_form_kwargs() + ) + + +class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): + def get_redirect_url(self, entity_slug, po_pk, *args, **kwargs): + return reverse('purchase_order_update', + kwargs={ + 'entity_slug': entity_slug, + 'po_pk': po_pk + }) + def get(self, request, *args, **kwargs): + kwargs['user_model'] = self.request.user + if not self.action_name: + raise ImproperlyConfigured('View attribute action_name is required.') + response = super(BasePurchaseOrderActionActionView, self).get(request, *args, **kwargs) + po_model: PurchaseOrderModel = self.get_object() + + try: + getattr(po_model, self.action_name)(commit=self.commit, **kwargs) + except ValidationError as e: + messages.add_message(request, + message=e.message, + level=messages.ERROR, + ) + return response + +class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): + template_name = 'purchase_orders/po_delete.html' + + def get_success_url(self): + return reverse('purchase_order_list') + + +class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_draft' + + +class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_review' + + +class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_approved' + + +class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_fulfilled' + + +class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_canceled' + + +class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView): + action_name = 'mark_as_void' + + +##############################bil + +class BillModelCreateViewView(BillModelCreateView): + template_name = 'ledger/bills/bill_form.html' \ No newline at end of file diff --git a/templates/bill/bill_create.html b/templates/bill/bill_create.html new file mode 100644 index 00000000..44ca7ed8 --- /dev/null +++ b/templates/bill/bill_create.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load django_ledger %} + +{% block content %} +
+
+
+
+

{% 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|add_class:"form-control" }} +
+
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/partials/vehicle_dropdowns.html b/templates/partials/vehicle_dropdowns.html new file mode 100644 index 00000000..e4494c10 --- /dev/null +++ b/templates/partials/vehicle_dropdowns.html @@ -0,0 +1,135 @@ + +{% extends "base.html" %} + +{% block content %} +
+

{{ title }}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+

Items

+ {{ formset.management_form }} + +
+ {% for form in formset %} +
+ {{ form.id }} +
+ {{ form.make.label_tag }} + {{ form.make }} +
+
+ {{ form.model.label_tag }} + {{ form.model }} +
+
+ {{ form.serie.label_tag }} + {{ form.serie }} +
+
+ {{ form.trim.label_tag }} + {{ form.trim }} +
+
+ {{ form.year.label_tag }} + {{ form.year }} +
+
+ {{ form.color.label_tag }} + {{ form.color }} +
+
+ {{ form.expected_cost.label_tag }} + {{ form.expected_cost }} +
+
+ +
+
+ {% endfor %} +
+ + + +
+ + Cancel +
+
+
+ + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/purchase_orders/includes/card_po.html b/templates/purchase_orders/includes/card_po.html new file mode 100644 index 00000000..ed963fea --- /dev/null +++ b/templates/purchase_orders/includes/card_po.html @@ -0,0 +1,237 @@ +{% load i18n %} +{% load django_ledger %} +{% load widget_tweaks %} + + +{% if not create_po %} + {% if style == 'po-detail' %} +
+
+
+ + {% icon 'uil:bill' 36 %} + +

+ {{ po_model.po_number }} +

+
+
+ +
+

+ + {{ po_model.get_po_status_display }} + +

+ + {# Display PO Contract Information #} + {% if po_model.is_contract_bound %} +
+ +
+

{% trans 'Contract' %}

+

{{ po_model.ce_model.estimate_number }}

+
+ + {% trans 'View Contract' %} + +
+ {% endif %} + +
+ {% if po_model.is_draft %} +
+
+
+
{% trans 'Draft Date' %}
+

{{ po_model.date_draft|date }}

+
+
+
+
+
+
+
{% trans 'Purchase Order Amount' %}
+

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+
+
+
+ {% endif %} + + {% if po_model.is_review %} +
+
+
+
{% trans 'Review Date' %}
+

{{ po_model.date_in_review|date }}

+
+
+
+
+
+
+
{% trans 'Purchase Order Amount' %}
+

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+
+
+
+ {% endif %} + + {% if po_model.is_approved %} +
+
+
+
{% trans 'Approved Date' %}
+

{{ po_model.date_approved|date }}

+
+
+
+
+
+
+
{% trans 'PO Amount' %}
+

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+
+
+
+
+
+
+
{% trans 'Received Amount' %}
+

{% currency_symbol %}{{ po_model.po_amount_received|currency_format }}

+
+
+
+ {% endif %} + + {% if po_model.is_fulfilled %} +
+
+
+
{% trans 'Fulfilled Date' %}
+

{{ po_model.date_fulfilled|date }}

+
+
+
+
+
+
+
{% trans 'PO Amount' %}
+
+

{% currency_symbol %}{{ po_model.po_amount|currency_format }}

+ + {% trans 'Fulfilled' %} + +
+
+
+
+ {% endif %} +
+
+ + +
+ {% endif %} +{% else %} + +{% endif %} \ No newline at end of file diff --git a/templates/purchase_orders/includes/inventory_item_form.html b/templates/purchase_orders/includes/inventory_item_form.html new file mode 100644 index 00000000..2841e8e5 --- /dev/null +++ b/templates/purchase_orders/includes/inventory_item_form.html @@ -0,0 +1,21 @@ + +
+ {% csrf_token %} + {% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %} + {% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %} + {% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %} + {% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %} +
+ + +
+
+ + +
+ +
diff --git a/templates/purchase_orders/includes/mark_as.html b/templates/purchase_orders/includes/mark_as.html new file mode 100644 index 00000000..3d1497ec --- /dev/null +++ b/templates/purchase_orders/includes/mark_as.html @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/templates/purchase_orders/includes/po_item_formset.html b/templates/purchase_orders/includes/po_item_formset.html new file mode 100644 index 00000000..fc3ebe53 --- /dev/null +++ b/templates/purchase_orders/includes/po_item_formset.html @@ -0,0 +1,101 @@ +{% load trans from i18n %} +{% load django_ledger %} +{% load widget_tweaks %} + +
+
+
+

{% trans 'Purchase Order Items' %}

+
+
+
+ {% csrf_token %} + {{ itemtxs_formset.non_form_errors }} + {{ itemtxs_formset.management_form }} + + + + + + + + + + {% if itemtxs_formset.can_delete %} + + {% endif %} + + + + + + {% for f in itemtxs_formset %} + + + + + + + + + {% if itemtxs_formset.can_delete %} + + {% endif %} + + + + {% endfor %} + + + + + + + + + + + {% if itemtxs_formset.can_delete %} + + {% endif %} + + + + +
{% trans 'Item' %}{% trans 'Unit Cost' %}{% trans 'Quantity' %}{% trans 'Unit' %}{% trans 'Amount' %}{% trans 'Status' %}{% trans 'Delete' %}{% trans 'Create Bill' %}{% trans 'Bill Paid?' %}
+ {% for hidden_field in f.hidden_fields %} + {{ hidden_field }} + {% endfor %} + {{ f.item_model|add_class:"form-control" }} + {% if f.errors %} +
{{ f.errors }}
+ {% endif %} +
{{ f.po_unit_cost|add_class:"form-control" }}{{ f.po_quantity|add_class:"form-control" }}{{ f.entity_unit|add_class:"form-control" }} + {% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}{{ f.po_item_status|add_class:"form-control" }} + {{ f.DELETE|add_class:"form-check-input" }} + + {% if f.instance.can_create_bill %} + {{ f.create_bill|add_class:"form-check-input" }} + {% elif f.instance.bill_model %} + + {% trans 'View Bill' %} + + {% endif %} + + {% if f.instance.bill_model %} + {% if f.instance.bill_model.is_paid %} + {% icon 'bi:check-circle-fill' 24 %} + {% else %} + {% icon 'clarity:no-access-solid' 24 %} + {% endif %} + {% endif %} +
{% trans 'Total' %}{% currency_symbol %}{{ po_model.po_amount | currency_format }}
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/templates/purchase_orders/includes/po_table.html b/templates/purchase_orders/includes/po_table.html new file mode 100644 index 00000000..1d4d7f04 --- /dev/null +++ b/templates/purchase_orders/includes/po_table.html @@ -0,0 +1,50 @@ +{% load django_ledger %} +{% load i18n %} + +
+ + + + + + + + + + + + + + {% for po in po_list %} + + + + + + + + + {% endfor %} + +
PO NumberDescriptionStatus DatePO StatusPO AmountActions
{% if po.po_number %}{{ po.po_number }}{% endif %}{{ po.po_title }}{{ po.get_status_action_date }}{{ po.get_po_status_display }}{% currency_symbol %}{{ po.po_amount | currency_format }} + +
+ +
diff --git a/templates/purchase_orders/inventory_item_form.html b/templates/purchase_orders/inventory_item_form.html new file mode 100644 index 00000000..307f68eb --- /dev/null +++ b/templates/purchase_orders/inventory_item_form.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_filters %} +{% block title %} + {# Check if an 'object' exists in the context #} + {% if object %} + {% trans 'Update Inventory Item'%} + {% else %} + {% trans 'Add New Inventory Item'%} + {% endif %} +{% endblock %} +{% block content %} +
+
+
+
+ +

+ {% if vendor.created %} + + {{ _("Edit Inventory Item") }} + {% else %} + + {{ _("Add New Inventory Item") }} + {% endif %} +

+
+
+
+ +
+
+ +
+ {% csrf_token %} + {{ redirect_field }} + {{ form|crispy }} + {% for error in form.errors %} +
{{ error }}
+ {% endfor %} +
+ + {% trans "Cancel" %} +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/purchase_orders/partials/po-select.html b/templates/purchase_orders/partials/po-select.html new file mode 100644 index 00000000..8f68abfb --- /dev/null +++ b/templates/purchase_orders/partials/po-select.html @@ -0,0 +1,13 @@ +
+ + +
\ No newline at end of file diff --git a/templates/purchase_orders/po_confirm_delete.html b/templates/purchase_orders/po_confirm_delete.html new file mode 100644 index 00000000..c27c10c7 --- /dev/null +++ b/templates/purchase_orders/po_confirm_delete.html @@ -0,0 +1,19 @@ + +{% extends "base.html" %} + +{% block title %} + Confirm Delete - {{ block.super }} +{% endblock %} + +{% block content %} +
+

Confirm Deletion

+

Are you sure you want to delete the Purchase Order "{{ po.po_number }}"?

+ +
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/purchase_orders/po_delete.html b/templates/purchase_orders/po_delete.html new file mode 100644 index 00000000..c24c7a61 --- /dev/null +++ b/templates/purchase_orders/po_delete.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load django_ledger %} + +{% block content %} +
+
+
+ {% csrf_token %} +
+
+

Are you sure you want to delete + Purchase Order {{ po_model.po_number }}?

+ +

All transactions associated with this Purchase Order will be deleted. + If you want to void the PO instead, click here

+ +
+ {% trans 'Go Back' %} + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/purchase_orders/po_detail.html b/templates/purchase_orders/po_detail.html new file mode 100644 index 00000000..7674201c --- /dev/null +++ b/templates/purchase_orders/po_detail.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% load trans from i18n %} +{% load static %} +{% load custom_filters %} +{% load django_ledger %} + +{% block content %} +
+
+ +
+
+ +
+
+ {% include 'purchase_orders/includes/card_po.html' with po_model=po_model entity_slug=entity_slug style='po-detail' %} +
+
+ + + + {% trans 'PO List' %} + +
+
+ + +
+ +
+
+
+
+
+
{% trans 'PO Amount' %}
+

+ {% currency_symbol %}{{ po_model.po_amount | absolute | currency_format }} +

+
+
+
+
+
{% trans 'Amount Received' %}
+

+ {% currency_symbol %}{{ po_model.po_amount_received | currency_format }} +

+
+
+
+
+
+ + +
+
+

{{ po_model.po_title }}

+ + +
+ {% po_item_table1 po_items %} +
+
+
+
+
+
+ +{% include "purchase_orders/includes/mark_as.html" %} +{% endblock %} + +{% block customJS %} + +{% endblock customJS %} \ No newline at end of file diff --git a/templates/purchase_orders/po_detail_backup.html b/templates/purchase_orders/po_detail_backup.html new file mode 100644 index 00000000..41e53dfb --- /dev/null +++ b/templates/purchase_orders/po_detail_backup.html @@ -0,0 +1,95 @@ + +{% extends "base.html" %} +{% load static i18n %} +{% block title %} + {{ po.po_number }} - Purchase Order - {{ block.super }} +{% endblock %} + +{% block content %} +
+

Purchase Order: {{ po.po_number }}

+

Status: + + {{ po.po_status }} + +

+ +
+
+
+
Supplier
+
{{ po.supplier.name }}
+ +
Created At
+
{{ po.created|date:"M d, Y H:i" }}
+ +
Total Amount
+
${{ po.total_amount|floatformat:2 }}
+
+
+
+ +

Ordered Items

+ + + + + + + + + + + {% for item in inventory_items %} + + + + + + + {% empty %} + + {% endfor %} + +
Item NumberItemUnit of MeasureAccount
{{ item.item_number }}{{ item.name }}${{ item.uom.name }}${{ item.inventory_account }}
No items found.
+ +
+ + + + Back to List + +
+
+