new changes #264

Merged
ismail merged 6 commits from frontend into main 2025-09-15 17:41:59 +03:00
18 changed files with 325 additions and 178 deletions

View File

@ -6,7 +6,9 @@ from django.contrib.auth.models import Permission
from inventory.validators import SaudiPhoneNumberValidator from inventory.validators import SaudiPhoneNumberValidator
from decimal import Decimal from decimal import Decimal
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify
# from django.utils.text import slugify
from slugify import slugify
from django.utils import timezone from django.utils import timezone
from django.core.validators import MinValueValidator,MaxValueValidator from django.core.validators import MinValueValidator,MaxValueValidator
import hashlib import hashlib
@ -59,10 +61,11 @@ from encrypted_model_fields.fields import (
# from simple_history.models import HistoricalRecords # from simple_history.models import HistoricalRecords
from plans.models import Invoice from plans.models import Invoice
from django_extensions.db.fields import RandomCharField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
class Base(models.Model): class Base(models.Model):
id = models.UUIDField( id = models.UUIDField(
unique=True, unique=True,
@ -644,7 +647,7 @@ class Car(Base):
null=True, null=True,
blank=True, blank=True,
) )
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) vin = models.CharField(max_length=17, verbose_name=_("VIN"))
dealer = models.ForeignKey( dealer = models.ForeignKey(
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer") "Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
) )
@ -771,6 +774,11 @@ class Car(Base):
condition=Q(status=CarStatusChoices.AVAILABLE), condition=Q(status=CarStatusChoices.AVAILABLE),
), ),
] ]
constraints = [
models.UniqueConstraint(
fields=["dealer", "vin"], name="unique_vin_per_dealer"
)
]
def __str__(self): def __str__(self):
make = self.id_car_make.name if self.id_car_make else "Unknown Make" make = self.id_car_make.name if self.id_car_make else "Unknown Make"
@ -1519,25 +1527,11 @@ class Staff(models.Model):
active = models.BooleanField(default=True, verbose_name=_("Active")) active = models.BooleanField(default=True, verbose_name=_("Active"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField( # slug = models.SlugField(
max_length=255, unique=True, editable=False, null=True, blank=True # max_length=255, unique=True, editable=False, null=True, blank=True,allow_unicode=True
) # )
slug = RandomCharField(length=8, unique=True)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.first_name}-{self.last_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
objects = StaffUserManager() objects = StaffUserManager()
@property @property
@ -1760,24 +1754,7 @@ class Customer(models.Model):
) )
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField( slug = RandomCharField(length=8, unique=True)
max_length=255, unique=True, editable=False, null=True, blank=True
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.last_name} {self.first_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta: class Meta:
constraints = [ constraints = [
@ -1932,24 +1909,7 @@ class Organization(models.Model, LocalizedNameMixin):
active = models.BooleanField(default=True, verbose_name=_("Active")) active = models.BooleanField(default=True, verbose_name=_("Active"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField( slug = RandomCharField(length=8, unique=True)
max_length=255, unique=True, editable=False, null=True, blank=True
)
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("Organization") verbose_name = _("Organization")
@ -2151,7 +2111,7 @@ class Lead(models.Model):
auto_now_add=True, verbose_name=_("Created"), db_index=True auto_now_add=True, verbose_name=_("Created"), db_index=True
) )
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(unique=True, blank=True, null=True) slug = RandomCharField(length=8, unique=True)
class Meta: class Meta:
verbose_name = _("Lead") verbose_name = _("Lead")
@ -2272,21 +2232,6 @@ class Lead(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("lead_detail", args=[self.dealer.slug, self.slug]) return reverse("lead_detail", args=[self.dealer.slug, self.slug])
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(f"{self.last_name} {self.first_name}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Schedule(models.Model): class Schedule(models.Model):
PURPOSE_CHOICES = [ PURPOSE_CHOICES = [
@ -2485,13 +2430,8 @@ class Opportunity(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
slug = models.SlugField( slug = RandomCharField(length=8, unique=True)
null=True,
blank=True,
unique=True,
verbose_name=_("Slug"),
help_text=_("Unique slug for the opportunity."),
)
loss_reason = models.CharField(max_length=255, blank=True, null=True) loss_reason = models.CharField(max_length=255, blank=True, null=True)
def get_notes(self): def get_notes(self):
@ -2534,29 +2474,6 @@ class Opportunity(models.Model):
objects = objects.union(lead_objects).order_by("-created") objects = objects.union(lead_objects).order_by("-created")
return objects return objects
def save(self, *args, **kwargs):
opportinity_for = ""
if self.lead.lead_type == "customer":
self.customer = self.lead.customer
opportinity_for = self.customer.first_name + " " + self.customer.last_name
elif self.lead.lead_type == "organization":
self.organization = self.lead.organization
opportinity_for = self.organization.name
if not self.slug:
base_slug = slugify(f"opportinity {opportinity_for}")
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("Opportunity") verbose_name = _("Opportunity")
verbose_name_plural = _("Opportunities") verbose_name_plural = _("Opportunities")
@ -2809,30 +2726,13 @@ class Vendor(models.Model, LocalizedNameMixin):
) )
active = models.BooleanField(default=True, verbose_name=_("Active")) active = models.BooleanField(default=True, verbose_name=_("Active"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
slug = models.SlugField( slug = RandomCharField(length=8, unique=True)
max_length=255, unique=True, verbose_name=_("Slug"), null=True, blank=True
)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"vendor_detail", kwargs={"dealer_slug": self.dealer.slug, "slug": self.slug} "vendor_detail", kwargs={"dealer_slug": self.dealer.slug, "slug": self.slug}
) )
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.name)
self.slug = base_slug
counter = 1
while (
self.__class__.objects.filter(slug=self.slug)
.exclude(pk=self.pk)
.exists()
):
self.slug = f"{base_slug}-{counter}"
counter += 1
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("Vendor") verbose_name = _("Vendor")
verbose_name_plural = _("Vendors") verbose_name_plural = _("Vendors")

View File

@ -938,7 +938,12 @@ urlpatterns = [
views.ItemServiceUpdateView.as_view(), views.ItemServiceUpdateView.as_view(),
name="item_service_update", name="item_service_update",
), ),
path(
"<slug:dealer_slug>/items/services/<int:pk>/detail/",
views.ItemServiceDetailView.as_view(),
name="item_service_detail",
),
# Expanese # Expanese
path( path(
"<slug:dealer_slug>/items/expeneses/", "<slug:dealer_slug>/items/expeneses/",
@ -955,6 +960,11 @@ urlpatterns = [
views.ItemExpenseUpdateView.as_view(), views.ItemExpenseUpdateView.as_view(),
name="item_expense_update", name="item_expense_update",
), ),
path(
"<slug:dealer_slug>/items/expeneses/<uuid:pk>/detail/",
views.ItemExpenseDetailView.as_view(),
name="item_expense_detail",
),
# Bills # Bills
path( path(
"<slug:dealer_slug>/items/bills/", "<slug:dealer_slug>/items/bills/",

View File

@ -25,7 +25,9 @@ from django_ledger.models import (
InvoiceModel, InvoiceModel,
BillModel, BillModel,
VendorModel, VendorModel,
AccountModel AccountModel,
EntityModel,
ChartOfAccountModel
) )
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django_ledger.models.items import ItemModel from django_ledger.models.items import ItemModel
@ -1594,10 +1596,13 @@ def _post_sale_and_cogs(invoice, dealer):
1) Cash / A-R / VAT / Revenue journal 1) Cash / A-R / VAT / Revenue journal
2) COGS / Inventory journal 2) COGS / Inventory journal
""" """
entity = invoice.ledger.entity entity:EntityModel = invoice.ledger.entity
# calc = CarFinanceCalculator(invoice) # calc = CarFinanceCalculator(invoice)
data = get_finance_data(invoice, dealer) data = get_finance_data(invoice, dealer)
car = data.get("car") car = data.get("car")
coa:ChartOfAccountModel = entity.get_default_coa()
# cash_acc = ( # cash_acc = (
# entity.get_default_coa_accounts() # entity.get_default_coa_accounts()
# .filter(role_default=True, role=roles.ASSET_CA_CASH) # .filter(role_default=True, role=roles.ASSET_CA_CASH)
@ -1610,7 +1615,26 @@ def _post_sale_and_cogs(invoice, dealer):
car_rev = dealer.settings.invoice_vehicle_sale_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() car_rev = dealer.settings.invoice_vehicle_sale_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
add_rev = dealer.settings.invoice_additional_services_account add_rev = dealer.settings.invoice_additional_services_account
if not add_rev:
try:
add_rev = entity.get_default_coa_accounts().filter(name="After-Sales Services", active=True).first()
if not add_rev:
add_rev = coa.create_account(
code="4020",
name="After-Sales Services",
role=roles.INCOME_OPERATIONAL,
balance_type=roles.CREDIT,
active=True,
)
add_rev.role_default = False
add_rev.save(update_fields=['role_default'])
dealer.settings.invoice_additional_services_account = add_rev
dealer.settings.save()
except Exception as e:
logger.error(f"error find or create additional services account {e}")
if car.get_additional_services_amount > 0 and not add_rev:
raise Exception("additional services exist but not account found,please create account for the additional services and set as default in the settings")
cogs_acc = dealer.settings.invoice_cost_of_good_sold_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() cogs_acc = dealer.settings.invoice_cost_of_good_sold_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first()
inv_acc = dealer.settings.invoice_inventory_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() inv_acc = dealer.settings.invoice_inventory_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
@ -1672,7 +1696,7 @@ def _post_sale_and_cogs(invoice, dealer):
if car.get_additional_services_amount > 0: if car.get_additional_services_amount > 0:
# Cr Sales Additional Services # Cr Sales Additional Services
if not add_rev: if not add_rev:
logger.warning(f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry.") logger.warning(f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry.")
else: else:
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=je_sale, journal_entry=je_sale,

View File

@ -776,8 +776,7 @@ def aging_inventory_list_view(request, dealer_slug):
dealer=dealer, dealer=dealer,
receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days)
).exclude(status='sold') ).exclude(status='sold')
total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total']
# Apply filters to the queryset if they exist. Chaining is fine here. # Apply filters to the queryset if they exist. Chaining is fine here.
if selected_make: if selected_make:
aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make) aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make)
@ -790,6 +789,9 @@ def aging_inventory_list_view(request, dealer_slug):
if selected_stock_type: if selected_stock_type:
aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type) aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type)
total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total']
count_of_aging_cars = aging_cars_queryset.count()
# Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset. # Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset.
# This ensures all possible filter options are always available. # This ensures all possible filter options are always available.
@ -827,7 +829,9 @@ def aging_inventory_list_view(request, dealer_slug):
'all_series': all_series, 'all_series': all_series,
'all_stock_types': all_stock_types, 'all_stock_types': all_stock_types,
'all_years': all_years, 'all_years': all_years,
'total_aging_inventory_value':total_aging_inventory_value 'total_aging_inventory_value':total_aging_inventory_value,
'page_obj':page_obj,
'count_of_aging_cars':count_of_aging_cars
} }
@ -7749,7 +7753,7 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
model = models.AdditionalServices model = models.AdditionalServices
template_name = "items/service/service_list.html" template_name = "items/service/service_list.html"
context_object_name = "services" context_object_name = "services"
paginate_by = 30 paginate_by = 20
permission_required = ["inventory.view_additionalservices"] permission_required = ["inventory.view_additionalservices"]
def get_queryset(self): def get_queryset(self):
@ -7763,7 +7767,17 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
) )
return qs return qs
class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = models.AdditionalServices
template_name = "items/service/service_detail.html"
context_object_name = "service"
permission_required = ["inventory.view_additionalservices"]
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
sold_cars=models.Car.objects.filter(status='sold',)
context['total_services_price']=self.object.price*self.object.additionals.filter(status='sold').count()
return context
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
@ -7885,7 +7899,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
model = ItemModel model = ItemModel
template_name = "items/expenses/expenses_list.html" template_name = "items/expenses/expenses_list.html"
context_object_name = "expenses" context_object_name = "expenses"
paginate_by = 4 paginate_by =20
permission_required = ["django_ledger.view_itemmodel"] permission_required = ["django_ledger.view_itemmodel"]
def get_queryset(self): def get_queryset(self):
@ -7897,6 +7911,32 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
return qs return qs
class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
queryset=ItemModel.objects.filter(item_role='expense')
template_name = "items/expenses/expense_detail.html"
context_object_name = "expense"
permission_required = ["django_ledger.view_itemmodel"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get the related bills queryset
bills_list = self.object.billmodel_set.all().order_by('-created')
# Paginate the bills
paginator = Paginator(bills_list, 10) # Show 10 bills per page
page_number = self.request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Add the paginated bills to the context
context['page_obj'] = page_obj
context["entity"] = get_user_type(self.request).entity
return context
class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
""" """
Provides a view for listing bills. Provides a view for listing bills.
@ -7919,6 +7959,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = BillModel model = BillModel
template_name = "ledger/bills/bill_list.html" template_name = "ledger/bills/bill_list.html"
context_object_name = "bills" context_object_name = "bills"
paginate_by=20
permission_required = ["django_ledger.view_billmodel"] permission_required = ["django_ledger.view_billmodel"]
def get_queryset(self): def get_queryset(self):
@ -9835,13 +9876,14 @@ def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk):
def pricing_page(request, dealer_slug): def pricing_page(request, dealer_slug):
dealer=get_object_or_404(models.Dealer, slug=dealer_slug) dealer=get_object_or_404(models.Dealer, slug=dealer_slug)
vat = models.VatRate.objects.filter(dealer=dealer).first() vat = models.VatRate.objects.filter(dealer=dealer).first()
if not hasattr(dealer.user,'userplan') or dealer.is_plan_expired: now = datetime.now().date() + timedelta(days=15)
if not hasattr(dealer.user,'userplan') or dealer.is_plan_expired or dealer.user.userplan.expire <= now:
plan_list = PlanPricing.objects.annotate( plan_list = PlanPricing.objects.annotate(
price_with_tax=Round(F('price') * vat.rate + F('price'), 2) price_with_tax=Round(F('price') * vat.rate + F('price'), 2)
).all() ).all()
form = forms.PaymentPlanForm() form = forms.PaymentPlanForm()
return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form})
else: else:
messages.info(request,_("You already have an plan!!")) messages.info(request,_("You already have an plan!!"))
return redirect('home',dealer_slug=dealer_slug) return redirect('home',dealer_slug=dealer_slug)

View File

@ -72,6 +72,7 @@
{% endif %} {% endif %}
<div class="d-flex align-items-center mb-4"> <div class="d-flex align-items-center mb-4">
{% if opportunity.car.marked_price %} {% if opportunity.car.marked_price %}
<span class="">{% trans "Marked Price: " %}</span>
<h5 class="mb-0 me-4"> <h5 class="mb-0 me-4">
{{ opportunity.car.marked_price }} <span class="fw-light"><span class="icon-saudi_riyal"></span></span> {{ opportunity.car.marked_price }} <span class="fw-light"><span class="icon-saudi_riyal"></span></span>
</h5> </h5>
@ -370,7 +371,7 @@
{% if request.user.email == opportunity.staff.email %} {% if request.user.email == opportunity.staff.email %}
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{% trans "You" %}</div> <div class="ps-6 ps-sm-0 fw-semibold mb-0">{% trans "You" %}</div>
{% else %} {% else %}
<div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.staff.get_local_name }}</div> <div class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.staff.fullname }}</div>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -29,17 +29,22 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<!-- Filter Controls -->
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100" <div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
id="filter-container"> id="filter-container"
<!-- Search Input - Wider and properly aligned --> hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML">
<div class="search-box position-relative flex-grow-1 me-2" <div class="search-box position-relative flex-grow-1 me-2"
style="min-width: 200px"> style="min-width: 200px">
<form class="position-relative show" <form class="position-relative show"
id="search-form" id="search-form"
hx-get="" hx-get="."
hx-boost="false" hx-boost="true"
hx-trigger="keyup changed delay:500ms, search"> hx-trigger="keyup changed delay:500ms, search"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML">
<input name="q" <input name="q"
id="search-input" id="search-input"
class="form-control form-control-sm search-input search" class="form-control form-control-sm search-input search"
@ -56,11 +61,8 @@
{% endif %} {% endif %}
</form> </form>
</div> </div>
<!-- Filter Dropdowns - Aligned in a row -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100" <div class="d-flex flex-column flex-sm-row gap-3 w-100"
style="max-width: 400px"> style="max-width: 400px">
<!-- Stage Filter -->
<!-- Stage Filter -->
<div class="flex-grow-1"> <div class="flex-grow-1">
<select class="form-select" <select class="form-select"
name="stage" name="stage"
@ -69,7 +71,7 @@
hx-target="#opportunities-grid" hx-target="#opportunities-grid"
hx-select="#opportunities-grid" hx-select="#opportunities-grid"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select"> hx-include="#search-form input, select[name='sort']">
<option value="">{% trans "All Stages" %}</option> <option value="">{% trans "All Stages" %}</option>
{% for value, label in stage_choices %} {% for value, label in stage_choices %}
<option value="{{ value }}" <option value="{{ value }}"
@ -79,7 +81,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<!-- Sort Filter -->
<div class="flex-grow-1"> <div class="flex-grow-1">
<select class="form-select" <select class="form-select"
name="sort" name="sort"
@ -88,7 +89,7 @@
hx-target="#opportunities-grid" hx-target="#opportunities-grid"
hx-select="#opportunities-grid" hx-select="#opportunities-grid"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select"> hx-include="#search-form input, select[name='stage']">
<option value="newest" <option value="newest"
{% if request.GET.sort == 'newest' %}selected{% endif %}> {% if request.GET.sort == 'newest' %}selected{% endif %}>
{% trans "Newest First" %} {% trans "Newest First" %}
@ -112,8 +113,10 @@
{% include 'crm/opportunities/partials/opportunity_grid.html' %} {% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} {% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3"> <div id="pagination-container" class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div> <div class="d-flex">
{% include 'partials/pagination.html' %}
</div>
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
@ -125,17 +128,19 @@
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById("search-input"); const searchInput = document.getElementById("search-input");
const clearButton = document.getElementById("clear-search"); const clearButton = document.getElementById("clear-search");
const searchForm = document.getElementById("search-form");
if (clearButton) { if (clearButton) {
clearButton.addEventListener("click", function() { clearButton.addEventListener("click", function() {
// Clear the input field
searchInput.value = ""; searchInput.value = "";
// This clears the search and triggers the htmx search
// by submitting the form with an empty query. // Trigger HTMX search with a 'search' event
searchForm.submit(); // This uses the hx-trigger="search" on the form
// and prevents a full page reload.
searchInput.dispatchEvent(new Event('search', { bubbles: true }));
}); });
} }
}); });
</script> </script>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@ -67,7 +67,7 @@
{% else %} {% else %}
{{ opportunity.staff.fullname }} {{ opportunity.staff.fullname }}
</p> </p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="mb-3 w-100"> <table class="mb-3 w-100">

View File

@ -11,7 +11,7 @@
<div class="row align-items-center justify-content-between g-3 mb-4"> <div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto"> <div class="col-auto">
<h3 class="mb-0"> <h3 class="mb-0">
{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i> {% trans 'Customer details' %}<i class="fas fa-user ms-2 "></i>
</h3> </h3>
</div> </div>
<div class="col-auto d-flex gap-2"> <div class="col-auto d-flex gap-2">

View File

@ -80,7 +80,7 @@
{% if is_paginated %} {% if is_paginated %}
<div class="d-flex justify-content-between mb-4"> <div class="d-flex justify-content-between mb-4">
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span> <span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span> <span class="text-muted">{% trans "Total Aging Cars:" %} {{ count_of_aging_cars }}</span>
</div> </div>
{% endif %} {% endif %}
{% if cars %} {% if cars %}
@ -118,12 +118,11 @@
<div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div> <div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div>
</div> </div>
{% endif %} {% endif %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">
{% if is_paginated %} {% if is_paginated %}
{% include 'partials/pagination.html' %} {% include 'partials/pagination.html' %}
{% endif %} {% endif %}
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load tenhal_tag %}
{% block title %}{% trans "Expense Detail" %}{% endblock %}
{% block content %}
<section class="hero-section text-center py-5">
<div class="container">
<h1 class="display-4 fw-bold text-primary">{{ expense.name|title }}</h1>
<p class="lead text-muted">{% trans "Comprehensive details for your expense." %}</p>
</div>
</section>
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-11">
<div class="card shadow-lg border-0 rounded-4">
<div class="card-body p-4 p-md-5">
<h2 class="h4 fw-bold mb-4">{% trans "Expense Information" %} <span class="fs-7 ms-3"><a class="btn btn-phoenix-secondary" href="{% url 'item_expense_list' request.dealer.slug %}">Back To List</a></span></h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4 text-center text-md-start mb-5">
<div class="col">
<h6 class="text-uppercase text-muted small mb-1">SKU</h6>
<p class="fw-bold ">{{ expense.sku|default:"N/A" }}</p>
</div>
<div class="col">
<h6 class="text-uppercase text-muted small mb-1">UPC</h6>
<p class="fw-bold ">{{ expense.upc|default:"N/A" }}</p>
</div>
<div class="col">
<h6 class="text-uppercase text-muted small mb-1">{% trans "Default Amount" %}</h6>
<p class="fw-bold">{{ expense.default_amount }}<span class="icon-saudi_riyal ms-1"></span></p>
</div>
<div class="col">
<h6 class="text-uppercase text-muted small mb-1">{% trans "Expense Account" %}</h6>
<p class="fw-boldBI">{{ expense.expense_account }}</p>
</div>
</div>
<hr class="my-5">
<h2 class="h4 fw-bold mb-4">{% trans "Associated Bills" %}</h2>
{% if page_obj %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th scope="col">{% trans "Bill Number" %}</th>
<th scope="col" class="text-center">{% trans "Status" %}</th>
<th scope="col" class="text-center">{% trans "Vendor" %}</th>
<th scope="col" class="text-center">{% trans "Terms" %}</th>
<th scope="col" class="text-center">{% trans "Created On" %}</th>
</tr>
</thead>
<tbody>
{% for bill in page_obj%}
<tr>
<td>
<a href="{% url 'bill-detail' dealer_slug=request.dealer.slug entity_slug=entity.slug bill_pk=bill.pk %}" class="fw-bold text-decoration-none">
{{ bill.bill_number }}
</a>
</td>
<td class="text-center">
<span class="">{{ bill.get_bill_status_display }}</span>
</td>
<td class="text-center">{{ bill.vendor }}</td>
<td class="text-center">{{ bill.get_terms_display }}</td>
<td class="text-center">{{ bill.created|date:"M j, Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% else %}
<div class="alert alert-info text-center py-4">
{% trans "No bills are associated with this expense yet." %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -51,9 +51,12 @@
href="{% url 'item_expense_update' request.dealer.slug expense.pk %}"> href="{% url 'item_expense_update' request.dealer.slug expense.pk %}">
<i class="fa fa-edit me-2"></i>{% trans "Update" %} <i class="fa fa-edit me-2"></i>{% trans "Update" %}
</a> </a>
<a class="text-info dropdown-item" href="{% url 'bill-create' request.dealer.slug request.entity.slug %}"> <a class="text-primary dropdown-item" href="{% url 'bill-create' request.dealer.slug request.entity.slug %}">
<i class="fa fa-plus me-2"></i>{% trans "Create Expense Bill" %} <i class="fa fa-plus me-2"></i>{% trans "Create Expense Bill" %}
</a> </a>
<a class="text-info dropdown-item" href="{% url 'item_expense_detail' request.dealer.slug expense.pk %}">
<i class="fa fa-eye me-2"></i>{% trans "Expense Detail" %}
</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load tenhal_tag %}
{% block title %}{{ service.name|title }} - {% trans "My Company" %}{% endblock %}
{% block content %}
<section class="hero-section text-center py-5">
<div class="container">
<h1 class="display-5 fw-bold">{{ service.name|title }}</h1>
<p class=" mt-3">{{ service.description|default:"No description provided." }}</p>
</div>
</section>
<section class="py-5">
<div class="container">
<div class="card mb-5 p-4 p-lg-5">
<div class="card-body">
<h2 class="card-title fw-bold mb-4">{% trans "Service Details" %}</h2>
<div class="row g-4 mb-3">
<div class="col-lg-8"><a class="btn btn-phoenix-primary me-1" href="{% url 'item_service_update' request.dealer.slug service.pk %}"> Edit</a></div>
<div class="col-lg-4"><a class="btn btn-phoenix-secondary mb-2" href="{% url 'item_service_list' request.dealer.slug %}">Back To List</a></div>
</div>
<div class="row g-4 text-center text-md-start">
<div class="col-md-4">
<h6 class="text-uppercase text-muted small mb-1">{% trans 'Service Name' %}</h6>
<p class="fw-bold fs-7 mb-0">{{ service.name|capfirst }}</p>
</div>
<div class="col-md-4">
<h6 class="text-uppercase text-muted small mb-1">{% trans 'Price' %}</h6>
<p class="fw-bold fs-7 mb-0">{{ service.price }}<span class="icon-saudi_riyal ms-1"></span></p>
</div>
<div class="col-md-2">
<h6 class="text-uppercase text-muted small mb-1">{% trans 'Unit of Measure' %}</h6>
<p class="fw-bold fs-7 mb-0">{{ service.get_uom_display }}</p>
</div>
<div class="col-md-2">
<h6 class="text-uppercase text-muted small mb-1">{% trans 'Tax Status' %}</h6>
<p class="fw-bold fs-7 mb-0">{% if service.taxable %}{% trans 'Taxable' %}{% else %}{% trans 'Non Taxable' %}{% endif %}</p>
</div>
</div>
</div>
</div>
<div class="text-center my-5 py-5 border rounded-3 p-4">
<h2 class="fw-bold text-muted mb-4">{% trans "Total Revenue from this service" %}</h2>
<p class="display-5 fw-bold mb-0 text-primary">{{ total_services_price }}<span class="icon-saudi_riyal ms-4"></span></p>
</div>
</div>
</section>
{% endblock %}

View File

@ -52,9 +52,9 @@
href="{% url 'item_service_update' request.dealer.slug service.pk %}"> href="{% url 'item_service_update' request.dealer.slug service.pk %}">
<i class="fa fa-edit me-2"></i>{% trans "Update" %} <i class="fa fa-edit me-2"></i>{% trans "Update" %}
</a> </a>
{% comment %} <a class="text-danger dropdown-item" href="#"> <a class="text-info dropdown-item" href="{% url 'item_service_detail' request.dealer.slug service.pk %}">
<i class="fa fa-trash me-2"></i>{% trans "Delete" %} <i class="fa fa-eye me-2"></i>{% trans "service detail" %}
</a> {% endcomment %} </a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -57,7 +57,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label for="end_date" class="form-label">{% trans 'End Date' %}</label> <label for="end_date" class="form-label">{% trans 'End Date' %}</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}"> <input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}">
</div> 65000.00 </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="make-select" class="form-label">{% trans 'Make' %}</label> <label for="make-select" class="form-label">{% trans 'Make' %}</label>
<select id="make-select" name="make" class="form-select"> <select id="make-select" name="make" class="form-select">

View File

@ -6,19 +6,19 @@
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom-0 py-3"> <div class="card-header border-bottom-0 py-3">
<h5 class="mb-0 fw-semibold">{% trans "Your Account" %}</h5> <h5 class="mb-0 fw-semibold">{% trans "Your Account" %}</h5>
</div> </div>
<div class="card-body pt-0"> <div class="card-body pt-0">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100"> <div class="d-flex flex-column bg-gray-200 rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Account" %}</span> <span class="text-muted small">{% trans "Account" %}</span>
<span class="fw-semibold">{{ user.dealer.get_local_name }}</span> <span class="fw-semibold">{{ user.dealer.get_local_name }}</span>
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100"> <div class="d-flex flex-column bg-gray-200 rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Status" %}</span> <span class="text-muted small">{% trans "Status" %}</span>
{% if userplan.active %} {% if userplan.active %}
<span class="badge bg-success bg-opacity-10 text-success">{% trans "Active" %}</span> <span class="badge bg-success bg-opacity-10 text-success">{% trans "Active" %}</span>
@ -28,13 +28,13 @@
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100"> <div class="d-flex flex-column bg-gray-200 rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Active until" %}</span> <span class="text-muted small">{% trans "Active until" %}</span>
<span class="fw-semibold">{{ userplan.expire }}</span> <span class="fw-semibold">{{ userplan.expire }}</span>
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100"> <div class="d-flex flex-column bg-gray-200 rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Plan" %}</span> <span class="text-muted small">{% trans "Plan" %}</span>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold">{{ userplan.plan }}</span> <span class="fw-semibold">{{ userplan.plan }}</span>
@ -52,7 +52,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom-0 py-3"> <div class="card-header border-bottom-0 py-3">
<h5 class="mb-0 fw-semibold">{% trans "Plan Details" %}</h5> <h5 class="mb-0 fw-semibold">{% trans "Plan Details" %}</h5>
</div> </div>
<div class="card-body">{% include "plans/plan_table.html" %}</div> <div class="card-body">{% include "plans/plan_table.html" %}</div>

View File

@ -252,8 +252,8 @@
<td class="align-middle">{{ data.car.year }}</td> <td class="align-middle">{{ data.car.year }}</td>
<td class="align-middle">{{ data.car.vin }}</td> <td class="align-middle">{{ data.car.vin }}</td>
<td class="align-middle">1</td> <td class="align-middle">1</td>
<td class="align-middle ps-5">{{ data.car.marked_price }}</td> <td class="align-middle ps-5">{{ data.car.marked_price|floatformat:'g' }}<span class="icon-saudi_riyal"></span></td>
<td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price }}</td> <td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price|floatformat:'g'}}<span class="icon-saudi_riyal"></span></td>
</tr> </tr>
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td> <td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>

View File

@ -334,8 +334,8 @@
<td class="align-middle">{{ data.car.year }}</td> <td class="align-middle">{{ data.car.year }}</td>
<td class="align-middle">{{ data.car.vin }}</td> <td class="align-middle">{{ data.car.vin }}</td>
<td class="align-middle">1</td> <td class="align-middle">1</td>
<td class="align-middle ps-5">{{ data.car.marked_price }}</td> <td class="align-middle ps-5">{{ data.car.marked_price|floatformat:'g' }}<span class='icon-saudi_riyal'></span></td>
<td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price }}</td> <td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price |floatformat:'g'}}<span class='icon-saudi_riyal'></span></td>
</tr> </tr>
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td> <td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>

View File

@ -58,7 +58,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<a class="fw-bold text-decoration-none text-dark" <a class="fw-bold text-decoration-none"
href="{% url 'user_detail' request.dealer.slug user.slug %}"> href="{% url 'user_detail' request.dealer.slug user.slug %}">
{{ user.fullname }} {{ user.fullname }}
</a> </a>