diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py index 944b6da5..6a1928ce 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.1 on 2025-06-12 16:25 +# Generated by Django 5.2.1 on 2025-06-12 14:22 import django.db.models.deletion import django.utils.timezone diff --git a/inventory/forms.py b/inventory/forms.py index 84934642..7ace0c01 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -20,6 +20,9 @@ from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormB from django_ledger.forms.journal_entry import ( JournalEntryModelCreateForm as JournalEntryModelCreateFormBase, ) +import csv +from io import TextIOWrapper +from django.utils.translation import gettext_lazy as _ from .models import ( Dealer, @@ -1928,3 +1931,66 @@ class ItemInventoryForm(forms.Form): label=_("Trim"), ) + + + +##################################################################### + +class CSVUploadForm(forms.Form): + dealer = forms.ModelChoiceField( + queryset=Dealer.objects.all(), + label=_('Dealer'), + widget=forms.HiddenInput() + ) + vendor = forms.ModelChoiceField( + queryset=Vendor.objects.all(), + label=_('Vendor'), + widget=forms.Select(attrs={'class': 'form-select'}), + required=True + ) + year = forms.IntegerField( + label=_('Year'), + widget=forms.NumberInput(attrs={'class': 'form-control'}), + required=True + ) + exterior = forms.ModelChoiceField( + queryset=ExteriorColors.objects.all(), + label=_('Exterior Color'), + widget=forms.RadioSelect(attrs={'class': 'form-select'}), + required=True + ) + interior = forms.ModelChoiceField( + queryset=InteriorColors.objects.all(), + label=_('Interior Color'), + widget=forms.RadioSelect(attrs={'class': 'form-select'}), + required=True + ) + receiving_date = forms.DateField( + label=_('Receiving Date'), + widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + required=True + ) + + def clean_csv_file(self): + csv_file = self.cleaned_data['csv_file'] + if not csv_file.name.endswith('.csv'): + raise forms.ValidationError(_('File is not a CSV file')) + + # Read and validate CSV structure + try: + csv_data = TextIOWrapper(csv_file.file, encoding='utf-8') + reader = csv.DictReader(csv_data) + + required_fields = ['vin', 'make', 'model', 'year'] + if not all(field in reader.fieldnames for field in required_fields): + missing = set(required_fields) - set(reader.fieldnames) + raise forms.ValidationError( + _('CSV is missing required columns: %(missing)s'), + params={'missing': ', '.join(missing)} + ) + except Exception as e: + raise forms.ValidationError(_('Error reading CSV file: %(error)s') % {'error': str(e)}) + + # Reset file pointer for later processing + csv_file.file.seek(0) + return csv_file \ No newline at end of file diff --git a/inventory/models.py b/inventory/models.py index 3154f4b8..8fab2192 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -712,7 +712,9 @@ class Car(Base): .filter(name=f"Cogs:{self.id_car_make.name}") .first() ) - + def add_colors(self,exterior,interior): + self.colors = CarColors.objects.create(car=self,exterior=exterior,interior=interior) + self.save() class CarTransfer(models.Model): car = models.ForeignKey( diff --git a/inventory/urls.py b/inventory/urls.py index cb8f11b4..197cbcfc 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -254,6 +254,8 @@ urlpatterns = [ name="vendor_delete", ), # Car URLs + path('cars/upload_cars/', views.upload_cars, name='upload_cars'), + path('cars//upload_cars/', views.upload_cars, name='upload_cars'), path("cars/add/", views.CarCreateView.as_view(), name="car_add"), path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"), path( @@ -696,13 +698,13 @@ path( path("items/bills/", views.BillListView.as_view(), name="bill_list"), # path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"), path('items/bills//create/', - views.BillModelCreateViewView.as_view(), + views.BillModelCreateView.as_view(), name='bill-create'), path('items/bills//create/purchase-order//', - views.BillModelCreateViewView.as_view(for_purchase_order=True), + views.BillModelCreateView.as_view(for_purchase_order=True), name='bill-create-po'), path('items/bills//create/estimate//', - views.BillModelCreateViewView.as_view(for_estimate=True), + views.BillModelCreateView.as_view(for_estimate=True), name='bill-create-estimate'), path('items/bills//detail//', views.BillModelDetailViewView.as_view(), @@ -769,7 +771,6 @@ path( name="bill_mark_as_paid", ), - # orders path("orders/", views.OrderListView.as_view(), name="order_list_view"), @@ -858,7 +859,7 @@ path( path('management///permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'), path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'), - + ######### # Purchase Order path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'), diff --git a/inventory/views.py b/inventory/views.py index ef34571a..4320ae97 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,14 +1,19 @@ # Standard +import os +import io +import csv import cv2 import json import logging -from datetime import datetime -from time import sleep +import tempfile import numpy as np +from time import sleep # from rich import print from random import randint from decimal import Decimal +from io import TextIOWrapper from django.apps import apps +from datetime import datetime from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -17,6 +22,7 @@ from urllib.parse import urlparse, urlunparse from inventory.models import Status as LeadStatus from django.db import IntegrityError from background_task.models import Task +from django.views.generic import FormView from django.db.models.deletion import RestrictedError from django.http.response import StreamingHttpResponse from django.core.exceptions import ImproperlyConfigured, ValidationError @@ -26,7 +32,7 @@ from django.db.models import Q from django.conf import settings from django.db.models import Func from django.contrib import messages -from django.http import Http404, HttpResponseNotFound, HttpResponseRedirect, JsonResponse, HttpResponseForbidden +from django.http import Http404, HttpResponseBadRequest, HttpResponseNotFound, HttpResponseRedirect, JsonResponse, HttpResponseForbidden from django.forms import HiddenInput, ValidationError from django.shortcuts import HttpResponse @@ -85,15 +91,17 @@ from django_ledger.forms.bank_account import ( BankAccountUpdateForm, ) from django_ledger.views.bill import ( - BillModelCreateView, + # BillModelCreateView, BillModelDetailView, BillModelUpdateView, BaseBillActionView as BaseBillActionViewBase, + BillModelModelBaseView ) from django_ledger.forms.bill import ( ApprovedBillModelUpdateForm, InReviewBillModelUpdateForm, get_bill_itemtxs_formset_class, + BillModelCreateForm ) from django_ledger.forms.invoice import ( @@ -980,23 +988,23 @@ class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessM template_name = "inventory/add_colors.html" success_message = _("Car Colors details updated successfully") permission_required = ["inventory.change_car"] - + def get_object(self, queryset=None): """ Retrieves the CarColors instance associated with the Car slug from the URL. This ensures we are updating the colors for the correct car. """ - + # Get the car_slug from the URL keywords arguments slug = self.kwargs.get('slug') - + # If no car_slug is provided, it's an invalid request if not slug: # You might want to raise Http404 or a more specific error here raise ValueError("Car slug is required to identify the colors to update.") - + return get_object_or_404(models.CarColors, car__slug=slug) def get_success_url(self): @@ -1008,7 +1016,7 @@ class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessM return reverse("car_detail", kwargs={"slug": self.object.car.slug}) def get_context_data(self, **kwargs): - """ + """ Adds the related Car object to the template context. """ context = super().get_context_data(**kwargs) @@ -6118,6 +6126,7 @@ class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): permission_required = ["django_ledger.view_billmodel"] def get_context_data(self, **kwargs): + dealer = get_user_type(self.request) bill = kwargs.get("object") if bill.get_itemtxs_data(): txs = bill.get_itemtxs_data()[0] @@ -6133,6 +6142,7 @@ class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): kwargs["transactions"] = transactions kwargs["grand_total"] = grand_total + kwargs["entity"] = dealer.entity return super().get_context_data(**kwargs) @@ -6307,13 +6317,299 @@ def bill_mark_as_paid(request, pk): return redirect("bill_detail", pk=bill.pk) +# class BillModelCreateViewView(Create): +# template_name = 'bill/bill_create.html' -class BillModelCreateViewView(BillModelCreateView): +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# context["entity"] = get_user_type(self.request).entity +# return context + + +# def post(self, request, *args, **kwargs): +# """ +# Completely override the post method to ensure our form handling is called +# """ +# print("Custom post method called") # Debugging +# form = self.get_form() +# if form.is_valid(): +# return self.custom_form_valid(form) +# else: +# return self.form_invalid(form) + +# def custom_form_valid(self, form): +# """ +# Our own form_valid implementation that will definitely be called +# """ +# print("Custom form_valid called") # Debugging + +# # Handle PO case +# if self.for_purchase_order: +# print("Handling PO case") # Debugging +# return self.handle_po_case(form) + +# # Handle Estimate case +# if self.for_estimate: +# print("Handling Estimate case") # Debugging +# return self.handle_estimate_case(form) + +# # Default case +# print("Handling default case") # Debugging +# return self.handle_default_case(form) + +# def handle_po_case(self, form): +# """Special handling for purchase orders""" +# po_pk = self.kwargs['po_pk'] +# item_uuids = self.request.GET.get('item_uuids', '').split(',') + +# if not item_uuids or not all(item_uuids): +# return HttpResponseBadRequest() + +# # Create bill +# bill_model = form.save(commit=False) +# ledger_model, bill_model = bill_model.configure( +# entity_slug=self.AUTHORIZED_ENTITY_MODEL, +# commit_ledger=True +# ) + +# # Get PO +# po_qs = PurchaseOrderModel.objects.for_entity( +# entity_slug=self.kwargs['entity_slug'], +# user_model=self.request.user +# ) +# po_model = get_object_or_404(po_qs, uuid__exact=po_pk) + +# # Validate +# try: +# bill_model.can_bind_po(po_model, raise_exception=True) +# except ValidationError as e: +# messages.error(self.request, e.message) +# return self.render_to_response(self.get_context_data(form=form)) + +# # Update models +# po_model_items_qs = po_model.itemtransactionmodel_set.filter(uuid__in=item_uuids) +# if po_model.is_contract_bound(): +# bill_model.ce_model_id = po_model.ce_model_id + +# bill_model.update_amount_due() +# bill_model.get_state(commit=True) +# bill_model.clean() +# bill_model.save() +# po_model_items_qs.update(bill_model=bill_model) + +# # Redirect to our custom URL +# return HttpResponseRedirect( +# reverse('purchase_order_update', +# kwargs={ +# 'entity_slug': self.kwargs['entity_slug'], +# 'po_pk': po_pk +# }) +# ) + +# def handle_estimate_case(self, form): +# """Special handling for estimates""" +# bill_model = form.save(commit=False) +# ledger_model, bill_model = bill_model.configure( +# entity_slug=self.AUTHORIZED_ENTITY_MODEL, +# commit_ledger=True +# ) + +# ce_pk = self.kwargs['ce_pk'] +# estimate_model_qs = EstimateModel.objects.for_entity( +# entity_slug=self.kwargs['entity_slug'], +# user_model=self.request.user +# ) +# estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk) +# bill_model.bind_estimate(estimate_model=estimate_model, commit=True) + +# # Redirect to our custom URL +# return HttpResponseRedirect( +# reverse('estimate_detail', +# kwargs={ +# 'pk': ce_pk +# }) +# ) + +# def handle_default_case(self, form): +# """Default form handling""" +# bill_model = form.save(commit=False) +# ledger_model, bill_model = bill_model.configure( +# entity_slug=self.AUTHORIZED_ENTITY_MODEL, +# commit_ledger=True +# ) +# bill_model.save() + +# # Redirect to our custom URL +# return HttpResponseRedirect( +# reverse('bill-detail', +# kwargs={ +# 'entity_slug': self.kwargs['entity_slug'], +# 'bill_pk': bill_model.uuid +# }) +# ) +class BillModelCreateView(CreateView): template_name = 'bill/bill_create.html' + PAGE_TITLE = _('Create Bill') + extra_context = { + 'page_title': PAGE_TITLE, + 'header_title': PAGE_TITLE, + 'header_subtitle_icon': 'uil:bill' + } + for_purchase_order = False + for_estimate = False + + def get(self, request, **kwargs): + if not request.user.is_authenticated: + return HttpResponseForbidden() + + if self.for_estimate and 'ce_pk' in self.kwargs: + estimate_qs = EstimateModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user + ) + estimate_model: EstimateModel = get_object_or_404(estimate_qs, uuid__exact=self.kwargs['ce_pk']) + if not estimate_model.can_bind(): + return HttpResponseNotFound('404 Not Found') + return super(BillModelCreateView, self).get(request, **kwargs) + def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["entity"] = get_user_type(self.request).entity + context = super(BillModelCreateView, self).get_context_data(**kwargs) + + if self.for_purchase_order: + po_pk = self.kwargs['po_pk'] + po_item_uuids_qry_param = self.request.GET.get('item_uuids') + if po_item_uuids_qry_param: + try: + po_item_uuids = po_item_uuids_qry_param.split(',') + except: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + po_qs = PurchaseOrderModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user + ).prefetch_related('itemtransactionmodel_set') + po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) + po_itemtxs_qs = po_model.itemtransactionmodel_set.filter( + bill_model__isnull=True, + uuid__in=po_item_uuids + ) + context['po_model'] = po_model + context['po_itemtxs_qs'] = po_itemtxs_qs + form_action = reverse('bill-create-po', + kwargs={ + 'entity_slug': self.kwargs['entity_slug'], + 'po_pk': po_model.uuid + }) + f'?item_uuids={po_item_uuids_qry_param}' + elif self.for_estimate: + estimate_qs = EstimateModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user + ) + estimate_uuid = self.kwargs['ce_pk'] + estimate_model: EstimateModel = get_object_or_404(estimate_qs, uuid__exact=estimate_uuid) + form_action = reverse('bill-create-estimate', + kwargs={ + 'entity_slug': self.kwargs['entity_slug'], + 'ce_pk': estimate_model.uuid + }) + else: + form_action = reverse('bill-create', + kwargs={ + 'entity_slug': self.kwargs['entity_slug'], + }) + context['form_action_url'] = form_action return context + + def get_initial(self): + return { + 'date_draft': get_localdate() + } + + def get_form(self, form_class=None): + dealer = get_user_type(self.request) + return BillModelCreateForm( + entity_model=dealer.entity, + **self.get_form_kwargs() + ) + + def form_valid(self, form): + dealer = get_user_type(self.request) + bill_model: BillModel = form.save(commit=False) + ledger_model, bill_model = bill_model.configure( + entity_slug=dealer.entity.slug, + user_model=self.request.user, + commit_ledger=True + ) + + if self.for_estimate: + ce_pk = self.kwargs['ce_pk'] + estimate_model_qs = EstimateModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user) + + estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk) + bill_model.bind_estimate(estimate_model=estimate_model, commit=False) + + elif self.for_purchase_order: + po_pk = self.kwargs['po_pk'] + item_uuids = self.request.GET.get('item_uuids') + if not item_uuids: + return HttpResponseBadRequest() + item_uuids = item_uuids.split(',') + po_qs = PurchaseOrderModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user + ) + po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) + + try: + bill_model.can_bind_po(po_model, raise_exception=True) + except ValidationError as e: + messages.add_message(self.request, + message=e.message, + level=messages.ERROR, + extra_tags='is-danger') + return self.render_to_response(self.get_context_data(form=form)) + + po_model_items_qs = po_model.itemtransactionmodel_set.filter(uuid__in=item_uuids) + + if po_model.is_contract_bound(): + bill_model.ce_model_id = po_model.ce_model_id + + bill_model.update_amount_due() + bill_model.get_state(commit=True) + bill_model.clean() + bill_model.save() + po_model_items_qs.update(bill_model=bill_model) + return HttpResponseRedirect(self.get_success_url()) + + return super(BillModelCreateView, self).form_valid(form) + + def get_success_url(self): + entity_slug = self.kwargs['entity_slug'] + if self.for_purchase_order: + po_pk = self.kwargs['po_pk'] + return reverse('purchase_order_update', + kwargs={ + 'entity_slug': entity_slug, + 'po_pk': po_pk + }) + elif self.for_estimate: + return reverse('customer-estimate-detail', + kwargs={ + 'entity_slug': entity_slug, + 'ce_pk': self.kwargs['ce_pk'] + }) + bill_model: BillModel = self.object + return reverse('bill-detail', + kwargs={ + 'entity_slug': entity_slug, + 'bill_pk': bill_model.uuid + }) + + class BillModelDetailViewView(BillModelDetailView): template_name = 'bill/bill_detail.html' class BillModelUpdateViewView(BillModelUpdateView): @@ -8385,7 +8681,7 @@ def AuditLogDashboardView(request): page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) - + elif q == 'loginEvents': template_name = 'admin_management/auth_logs.html' @@ -8408,7 +8704,7 @@ def AuditLogDashboardView(request): # 1. Paginate the raw QuerySet FIRST paginator = Paginator(model_events_queryset, logs_per_page) - + try: # Get the page object, which contains only the raw QuerySet objects for the current page page_obj_raw = paginator.page(current_pagination_page) @@ -8462,18 +8758,18 @@ def AuditLogDashboardView(request): 'new': 'Invalid JSON in changed_fields' }) processed_model_events_for_page.append(event_data) - + # 3. Replace the object_list of the original page_obj with the processed data # This keeps all pagination properties (has_next, number, etc.) intact. page_obj_raw.object_list = processed_model_events_for_page page_obj = page_obj_raw # This will be passed to the context - + # Pass the final page object to the context context['page_obj'] = page_obj return render(request, template_name, context) - + def activate_account(request, content_type, slug): @@ -8533,6 +8829,7 @@ def PurchaseOrderCreateView(request): return render(request, "purchase_orders/po_form.html", {"form": form}) def InventoryItemCreateView(request): + for_po = request.GET.get('for_po') dealer = get_user_type(request) entity = dealer.entity coa = entity.get_default_coa() @@ -8552,13 +8849,16 @@ def InventoryItemCreateView(request): model = request.POST.get("model") serie = request.POST.get("serie") trim = request.POST.get("trim") + year = request.POST.get("year") + exterior = models.ExteriorColors.objects.get(pk=request.POST.get("exterior")) + interior = models.InteriorColors.objects.get(pk=request.POST.get("interior")) 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}" + inventory_name = f"{make_name.name} - {model_name.name} - {serie_name.name} - {trim_name.name} - {year} - {exterior.name} - {interior.name}" uom = entity.get_uom_all().get(name='Unit') entity.create_item_inventory( name=inventory_name, @@ -8569,6 +8869,10 @@ def InventoryItemCreateView(request): ) messages.success(request, _("Inventory item created successfully")) return redirect('purchase_order_list') + if for_po: + form = forms.CSVUploadForm() + context = {"make_data":models.CarMake.objects.all(),"inventory_accounts":inventory_accounts,"cogs_accounts":cogs_accounts,"form":form} + return render(request,'purchase_orders/car_inventory_item_form.html',context) return render(request,'purchase_orders/inventory_item_form.html',{"make_data":models.CarMake.objects.all(),"inventory_accounts":inventory_accounts,"cogs_accounts":cogs_accounts}) @@ -8878,4 +9182,64 @@ class BillModelActionUnlockLedgerView(BaseBillActionView): class BillModelActionForceMigrateView(BaseBillActionView): - action_name = 'migrate_state' \ No newline at end of file + action_name = 'migrate_state' + +############################################################### +############################################################### + +def upload_cars(request,po_pk=None): + dealer = get_user_type(request) + + if request.method == 'POST': + csv_file = request.FILES.get('csv_file') + year = request.POST.get("year") + receiving_date = datetime.strptime(request.POST.get("receiving_date"), "%Y-%m-%d") + receiving_date = timezone.make_aware(receiving_date) + + try: + make = models.CarMake.objects.get(pk=request.POST.get("make")) + model = models.CarModel.objects.get(pk=request.POST.get("model")) + serie = models.CarSerie.objects.get(pk=request.POST.get("serie")) + trim = models.CarTrim.objects.get(pk=request.POST.get("trim")) + vendor = models.Vendor.objects.get(pk=request.POST.get("vendor")) + exterior = models.ExteriorColors.objects.get(pk=request.POST.get("exterior")) + interior = models.InteriorColors.objects.get(pk=request.POST.get("interior")) + except Exception as e: + messages.error(request, f"Error processing CSV: {str(e)}") + if po_pk: + return redirect('upload_cars',po_pk=po_pk) + else: + return redirect('upload_cars') + if not csv_file.name.endswith('.csv'): + messages.error(request, "Please upload a CSV file") + return redirect('upload_cars') + try: + # Read the file content + file_content = csv_file.read().decode('utf-8') + csv_data = io.StringIO(file_content) + reader = csv.DictReader(csv_data) + cars_created = 0 + for row in reader: + car = models.Car.objects.create( + dealer=dealer, + vin=row['vin'], + id_car_make=make, + id_car_model=model, + id_car_serie=serie, + id_car_trim=trim, + year=year, + vendor=vendor, + receiving_date=receiving_date, + ) + car.add_colors(exterior=exterior, interior=interior) + cars_created += 1 + + messages.success(request, f"Successfully imported {cars_created} cars") + return redirect('car_list') # redirect to your car list view + + except Exception as e: + messages.error(request, f"Error processing CSV: {str(e)}") + form = forms.CSVUploadForm() + return render(request, 'csv_upload.html',{"make_data":models.CarMake.objects.all(),"form":form}) +############################################################### +############################################################### \ No newline at end of file diff --git a/templates/bill/bill_detail.html b/templates/bill/bill_detail.html index 63ded748..2c5ad097 100644 --- a/templates/bill/bill_detail.html +++ b/templates/bill/bill_detail.html @@ -271,5 +271,5 @@ - +{% include "bill/includes/mark_as.html" %} {% endblock %} \ No newline at end of file diff --git a/templates/csv_upload.html b/templates/csv_upload.html new file mode 100644 index 00000000..89d6206f --- /dev/null +++ b/templates/csv_upload.html @@ -0,0 +1,186 @@ +{% extends "base.html" %} +{% load static i18n %} + +{% block customCSS %} + +{% endblock customCSS %} + +{% block content %} +
+

Upload Cars CSV

+ + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +
+ {% 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 %} +
+
+
+
+ {{form.vendor.label}} + {{form.vendor}} +
+
+ {{form.year.label}} + {{form.year}} +
+
+ {{form.receiving_date.label}} + {{form.receiving_date}} +
+
+ +
+
+

{% trans 'Exterior Colors' %}

+
+ {% for color in form.fields.exterior.queryset %} +
+ +
+ {% endfor %} +
+ +

{% trans 'Interior Colors' %}

+
+ {% for color in form.fields.interior.queryset %} +
+ +
+ {% endfor %} +
+
+
+
+ + +
+ CSV should include columns: vin, make, model, year (required) +
+
+ + + Cancel +
+ +
+

CSV Format Example

+ + + + + + + + + + + + + + + + + +
vinmakemodelyear
1HGCM82633A123456HondaAccord2023
+ + Download Sample CSV + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index 08eb54e6..5a139f61 100644 --- a/templates/header.html +++ b/templates/header.html @@ -28,6 +28,20 @@ + + {% endif %}