update po

This commit is contained in:
ismail 2025-06-15 20:03:43 +03:00
parent 6e0808402a
commit 90fea4d256
15 changed files with 834 additions and 75 deletions

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -254,6 +254,8 @@ urlpatterns = [
name="vendor_delete",
),
# Car URLs
path('cars/upload_cars/', views.upload_cars, name='upload_cars'),
path('cars/<uuid:po_pk>/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/<slug:entity_slug>/create/',
views.BillModelCreateViewView.as_view(),
views.BillModelCreateView.as_view(),
name='bill-create'),
path('items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/',
views.BillModelCreateViewView.as_view(for_purchase_order=True),
views.BillModelCreateView.as_view(for_purchase_order=True),
name='bill-create-po'),
path('items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/',
views.BillModelCreateViewView.as_view(for_estimate=True),
views.BillModelCreateView.as_view(for_estimate=True),
name='bill-create-estimate'),
path('items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/',
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/<str:content_type>/<slug:slug>/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'),

View File

@ -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'
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})
###############################################################
###############################################################

View File

@ -271,5 +271,5 @@
</div>
</div>
</div>
{% include "bill/includes/mark_as.html" %}
{% endblock %}

186
templates/csv_upload.html Normal file
View File

@ -0,0 +1,186 @@
{% extends "base.html" %}
{% load static i18n %}
{% block customCSS %}
<style>
.color-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
width: 80px; /* Increased from 3rem for better visibility */
height: 80px; /* Increased from 3rem for better visibility */
margin-right: 10px;
}
.color-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.color-option {
display: block;
position: relative;
margin: 0;
padding: 0;
height: 100%;
}
.color-radio {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.color-radio:checked + .color-display {
border: 2px solid #0d6efd;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
.color-radio:focus + .color-display {
border-color: #86b7fe;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
.color-display {
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 10px;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.color-name {
background-color: rgba(255, 255, 255, 0.8);
padding: 2px 5px;
border-radius: 3px;
text-align: center;
width: 100%;
font-size: 0.8rem;
}
/* Added for better layout of color options */
.color-options-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="container mt-4">
<h2>Upload Cars CSV</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="post" enctype="multipart/form-data" class="mt-4">
{% csrf_token %}
<div class="row g-4">
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %}
</div>
</div>
<div class="row g-4">
<div class="col">
{{form.vendor.label}}
{{form.vendor}}
</div>
<div class="col">
{{form.year.label}}
{{form.year}}
</div>
<div class="col">
{{form.receiving_date.label}}
{{form.receiving_date}}
</div>
</div>
<div class="mb-3">
<div class="row g-4">
<p class="fs-5 mb-2">{% trans 'Exterior Colors' %}</p>
<div class="color-options-container">
{% for color in form.fields.exterior.queryset %}
<div class="color-card">
<label class="color-option">
<input class="color-radio" type="radio" name="exterior" value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
<div class="color-display" style="background-color: rgb({{ color.rgb }})">
<span class="color-name">{{ color.get_local_name }}</span>
</div>
</label>
</div>
{% endfor %}
</div>
<p class="fs-5 mb-2">{% trans 'Interior Colors' %}</p>
<div class="color-options-container">
{% for color in form.fields.interior.queryset %}
<div class="color-card">
<label class="color-option">
<input class="color-radio" type="radio" name="interior" value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="color-display" style="background-color: rgb({{ color.rgb }})">
<span class="color-name">{{ color.get_local_name }}</span>
</div>
</label>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="mb-3">
<label for="csv_file" class="form-label">CSV File</label>
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
<div class="form-text">
CSV should include columns: vin, make, model, year (required)
</div>
</div>
<button type="submit" class="btn btn-primary">Upload</button>
<a href="{% url 'car_list' %}" class="btn btn-secondary">Cancel</a>
</form>
<div class="mt-4">
<h4>CSV Format Example</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>vin</th>
<th>make</th>
<th>model</th>
<th>year</th>
</tr>
</thead>
<tbody>
<tr>
<td>1HGCM82633A123456</td>
<td>Honda</td>
<td>Accord</td>
<td>2023</td>
</tr>
</tbody>
</table>
<a href="{% static 'sample/cars_sample.csv' %}" class="btn btn-outline-primary">
Download Sample CSV
</a>
</div>
</div>
{% endblock %}

View File

@ -28,6 +28,20 @@
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'inventory_item_create' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add invenotry item"|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'purchase_order_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
<li class="nav-item">
@ -416,7 +430,7 @@
<div class="overflow-auto scrollbar" style="height: 10rem;">
<ul class="nav d-flex flex-column mb-2 pb-1">
{% if request.is_dealer %}
<li class="nav-item">
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
</li>
{% else %}

View File

@ -67,7 +67,9 @@
{% elif bill.is_approved %}
<span class="badge badge-phoenix badge-phoenix-success">
{% elif bill.is_paid %}
<span class="badge badge-phoenix badge-phoenix-success">
<span class="badge badge-phoenix badge-phoenix-success">
{% elif bill.is_canceled %}
<span class="badge badge-phoenix badge-phoenix-danger">
{% endif %}
{{ bill.bill_status }}
</span>
@ -79,10 +81,10 @@
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'bill_detail' bill.pk %}" class="dropdown-item text-success-dark">{% trans 'View' %}</a>
<a href="{% url 'bill-detail' entity_slug=entity.slug bill_pk=bill.pk %}" class="dropdown-item text-success-dark">{% trans 'View' %}</a>
</div>
</div>
</td>
</tr>
{% empty %}
<tr>

View File

@ -1,13 +1,103 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% block customCSS %}
<style>
.color-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
width: 80px; /* Increased from 3rem for better visibility */
height: 80px; /* Increased from 3rem for better visibility */
margin-right: 10px;
}
.color-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.color-option {
display: block;
position: relative;
margin: 0;
padding: 0;
height: 100%;
}
.color-radio {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.color-radio:checked + .color-display {
border: 2px solid #0d6efd;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
.color-radio:focus + .color-display {
border-color: #86b7fe;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
.color-display {
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 10px;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.color-name {
background-color: rgba(255, 255, 255, 0.8);
padding: 2px 5px;
border-radius: 3px;
text-align: center;
width: 100%;
font-size: 0.8rem;
}
/* Added for better layout of color options */
.color-options-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
</style>
{% endblock %}
{% block content %}
<form action="" method="post">
{% 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 %}
<div class="row g-4">
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
</div>
<div class="col">
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %}
</div>
</div>
<div class="row g-4">
<div class="col">
{{form.vendor.label}}
{{form.vendor}}
</div>
<div class="col">
{{form.year.label}}
{{form.year}}
</div>
</div>
<div class="form-group">
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
@ -16,6 +106,39 @@
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">Add New Item To Inventory</button>
<div class="row g-4 mt-4">
<div class="col">
<p class="fs-5 mb-2">{% trans 'Exterior Colors' %}</p>
<div class="color-options-container">
{% for color in form.fields.exterior.queryset %}
<div class="color-card">
<label class="color-option">
<input class="color-radio" type="radio" name="exterior" value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
<div class="color-display" style="background-color: rgb({{ color.rgb }})">
<span class="color-name">{{ color.get_local_name }}</span>
</div>
</label>
</div>
{% endfor %}
</div>
</div>
<div class="col">
<p class="fs-5 mb-2">{% trans 'Interior Colors' %}</p>
<div class="color-options-container">
{% for color in form.fields.interior.queryset %}
<div class="color-card">
<label class="color-option">
<input class="color-radio" type="radio" name="interior" value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="color-display" style="background-color: rgb({{ color.rgb }})">
<span class="color-name">{{ color.get_local_name }}</span>
</div>
</label>
</div>
{% endfor %}
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mt-5">Add New Item To Inventory</button>
</form>
{% endblock content %}

View File

@ -58,7 +58,7 @@ document.addEventListener('DOMContentLoaded', function() {
<h4 class="h6 mb-1">{% trans 'Contract' %}</h4>
<p class="mb-0">{{ po_model.ce_model.estimate_number }}</p>
</div>
<a href="{% url 'django_ledger:customer-estimate-detail' entity_slug=view.kwargs.entity_slug ce_pk=po_model.ce_model_id %}"
<a href="{% url 'estimate_detail' po_model.ce_model_id %}"
class="btn btn-sm btn-outline-info ms-auto">
{% trans 'View Contract' %}
</a>
@ -192,33 +192,28 @@ document.addEventListener('DOMContentLoaded', function() {
onclick="showPOModal('Fulfill PO', '{% url 'po-action-mark-as-fulfilled' entity_slug po_model.pk %}', 'Mark As Fulfilled')">
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
</button>
{% endif %}
{# Danger Action Buttons #}
{% if po_model.can_delete %}
<button class="btn btn-outline-danger"
onclick="showPOModal('Cancel PO', '{% url 'po-delete' entity_slug po_model.pk %}', 'Mark As Cancelled')">
<i class="fas fa-ban me-2"></i>{% trans 'Delete' %}
</button>
{% endif %}
{% if po_model.can_void %}
<button class="btn btn-outline-danger"
onclick="showPOModal('Void PO', '{% url 'po-action-mark-as-void' entity_slug po_model.pk %}', 'Mark As Void')">
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
</button>
{% endif %}
{% if po_model.can_cancel %}
<button class="btn btn-outline-danger"
onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' entity_slug po_model.pk %}', 'Mark As Cancelled')">
<i class="fas fa-ban me-2"></i>{% trans 'Cancel' %}
</button>
{% endif %}
{# Danger Action Buttons #}
{% if po_model.can_delete %}
<a class="btn btn-outline-danger" href="{% url 'po-delete' entity_slug po_model.pk %}">
<i class="fas fa-ban me-2"></i>{% trans 'Delete' %}
</a>
{% endif %}
{% if po_model.can_void %}
<button class="btn btn-outline-danger"
onclick="djLedger.toggleModal('{{ po_model.get_mark_as_void_html_id }}')">
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
</button>
{% modal_action_v2 bill po_model.get_mark_as_void_url po_model.get_mark_as_void_message po_model.get_mark_as_void_html_id %}
{% endif %}
{% if po_model.can_cancel %}
<button class="btn btn-outline-secondary"
onclick="djLedger.toggleModal('{{ po_model.get_mark_as_canceled_html_id }}')">
<i class="fas fa-window-close me-2"></i>{% trans 'Cancel' %}
</button>
{% modal_action_v2 bill po_model.get_mark_as_canceled_url po_model.get_mark_as_canceled_message po_model.get_mark_as_canceled_html_id %}
{% endif %}
</div>
</div>
@ -228,7 +223,7 @@ document.addEventListener('DOMContentLoaded', function() {
{% else %}
<div class="card border-0 shadow-sm text-center py-5">
<div class="card-body">
<a href="{% url 'django_ledger:po-create' entity_slug=entity_slug %}" class="text-decoration-none">
<a href="{% url 'purchase_order_create' %}" class="text-decoration-none">
<span class="text-muted mb-3 d-inline-block">{% icon "ic:baseline-add-circle-outline" 48 %}</span>
<h3 class="h4 text-muted">{% trans 'New Purchase Order' %}</h3>
</a>

View File

@ -34,9 +34,9 @@
</div>
<div class="dropdown-menu" id="dropdown-menu-{{ po.uuid }}" role="menu">
<div class="dropdown-content">
<a href="{% url 'django_ledger:po-detail' entity_slug=entity_slug po_pk=po.uuid %}"
<a href="{% url 'purchase_order_detail' po_pk=po.uuid %}"
class="dropdown-item has-text-success">Details</a>
<a href="{% url 'django_ledger:po-delete' entity_slug=entity_slug po_pk=po.uuid %}"
<a href="{% url 'po-delete' entity_slug=entity_slug po_pk=po.uuid %}"
class="dropdown-item has-text-weight-bold has-text-danger">{% trans ' Delete' %}</a>
</div>
</div>

View File

@ -15,7 +15,7 @@
Purchase Order {{ po_model.po_number }}?</h2>
<p class="card-text text-muted mb-4">All transactions associated with this Purchase Order will be deleted.
If you want to void the PO instead, <a href="{% url 'django_ledger:po-detail' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}" class="text-decoration-none">click here</a></p>
If you want to void the PO instead, <a href="{% url 'purchase_order_detail' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}" class="text-decoration-none">click here</a></p>
<div class="d-flex justify-content-center gap-3 mt-4">
<a href="{% url 'purchase_order_update' entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"

View File

@ -18,11 +18,15 @@
<h3 class="">
{{ _("Purchase Orders") |capfirst }}
</h2>
<a href="{% url 'purchase_order_create' %}"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{{ _("Create New PO") }}</a>
<div>
<a href="{% url 'purchase_order_create' %}"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{{ _("Create New PO") }}</a>
<a href="{% url 'inventory_item_create' %}?for_po=1"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{{ _("Create Inventory Item for PO") }}</a>
</div>
</div>
{% include "partials/search_box.html" %}
<div class="table-responsive px-1 scrollbar mt-3">
<table class= "table align-items-center table-flush table-hover">
<thead>
@ -31,13 +35,13 @@
<th class="sort white-space-nowrap align-middle" scope="col" style="width:40%">Description</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Status</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Created At</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Actions</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Actions</th>
</tr>
</thead>
<tbody class="list">
{% if purchase_orders %}
{% for po in purchase_orders %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle product white-space-nowrap">{{ po.po_number }}</td>
<td class="align-middle product white-space-nowrap">{{ po.po_title }}</td>
@ -48,11 +52,13 @@
</td>
<td class="align-middle product white-space-nowrap">{{ po.created|date:"M d, Y" }}</td>
<td class="align-middle product white-space-nowrap">
<a href="{% url 'purchase_order_detail' po.pk %}"
class="btn btn-sm btn-phoenix-success">
<i class="fa-regular fa-eye me-1"></i>
{% trans "view"|capfirst %}
</a>
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'purchase_order_detail' po.pk %}" class="dropdown-item text-success-dark">{% trans 'View' %}</a>
<a href="{% url 'upload_cars' po.pk %}" class="dropdown-item text-success-dark">{% trans 'Upload Data' %}</a>
</div>
</div>
</td>
</tr>
{%endfor%}

View File

@ -28,7 +28,7 @@
</td>
<td class="has-text-centered">{% if item.bill_model_id %}
<a class="is-small is-info button"
href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=item.bill_model_id %}">View
href="{% url 'bill-detail' entity_slug=entity_slug bill_pk=item.bill_model_id %}">View
Bill</a>{% endif %}
</td>
</tr>