fix the delay in caused by the notification and switch to channels

This commit is contained in:
ismail 2025-07-22 14:59:19 +03:00
parent cf49777f26
commit c03fe9d85a
60 changed files with 2169 additions and 849 deletions

View File

@ -10,16 +10,31 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
# asgi.py # asgi.py
import os import os
from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
import django
django.setup()
from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from api import routing from api import routing
from inventory.notifications.sse import NotificationSSEApp
from django.urls import re_path
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") # application = ProtocolTypeRouter(
# {
application = ProtocolTypeRouter( # "http": get_asgi_application(),
{ # # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
"http": get_asgi_application(), # }
"websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), # )
} application = ProtocolTypeRouter({
) "http": AuthMiddlewareStack(
URLRouter([
path("sse/notifications/", NotificationSSEApp()),
re_path(r"", get_asgi_application()), # All other routes go to Django
])
),
})

View File

@ -4,9 +4,9 @@ from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from inventory import views from inventory import views
# from debug_toolbar.toolbar import debug_toolbar_urls # from debug_toolbar.toolbar import debug_toolbar_urls
from inventory.notifications.sse import NotificationSSEApp
# import debug_toolbar # import debug_toolbar
from schema_graph.views import Schema from schema_graph.views import Schema
# from two_factor.urls import urlpatterns as tf_urls # from two_factor.urls import urlpatterns as tf_urls
@ -30,6 +30,7 @@ urlpatterns += i18n_patterns(
path("plans/", include("plans.urls")), path("plans/", include("plans.urls")),
path("schema/", Schema.as_view()), path("schema/", Schema.as_view()),
path("tours/", include("tours.urls")), path("tours/", include("tours.urls")),
# path('', include(tf_urls)), # path('', include(tf_urls)),
) )

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-07-15 11:38 # Generated by Django 5.2.4 on 2025-07-22 08:37
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-07-15 11:38 # Generated by Django 5.2.4 on 2025-07-22 08:37
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@ -430,25 +430,22 @@ class CarFinanceForm(forms.ModelForm):
additional services associated with a car finance application. additional services associated with a car finance application.
""" """
additional_finances = forms.ModelMultipleChoiceField( # additional_finances = forms.ModelMultipleChoiceField(
queryset=AdditionalServices.objects.all(), # queryset=AdditionalServices.objects.all(),
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), # widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
required=False, # required=False,
) # )
class Meta: class Meta:
model = CarFinance model = CarFinance
exclude = [ fields = ["cost_price","marked_price"]
"car",
"profit_margin",
"vat_amount",
"total",
"additional_services",
]
def save(self, commit=True): def save(self, commit=True):
instance = super().save() instance = super().save()
instance.additional_services.set(self.cleaned_data["additional_finances"]) try:
instance.additional_services.set(self.cleaned_data["additional_finances"])
except KeyError:
pass
instance.save() instance.save()
return instance return instance
@ -1362,27 +1359,27 @@ class SaleOrderForm(forms.ModelForm):
class Meta: class Meta:
model = SaleOrder model = SaleOrder
fields = [ fields = [
"customer", # "customer",
"expected_delivery_date", "expected_delivery_date",
"estimate", # "estimate",
"opportunity", # "opportunity",
"comments", "comments",
"order_date", # "order_date",
"status", # "status",
] ]
widgets = { widgets = {
"expected_delivery_date": forms.DateInput( "expected_delivery_date": forms.DateInput(
attrs={"type": "date", "label": _("Expected Delivery Date")} attrs={"type": "date", "label": _("Expected Delivery Date")}
), ),
"order_date": forms.DateInput( # "order_date": forms.DateInput(
attrs={"type": "date", "label": _("Order Date")} # attrs={"type": "date", "label": _("Order Date")}
), # ),
"customer": forms.Select( # "customer": forms.Select(
attrs={ # attrs={
"class": "form-control", # "class": "form-control",
"label": _("Customer"), # "label": _("Customer"),
} # }
), # ),
} }

View File

@ -0,0 +1,79 @@
from decimal import Decimal
import random
from django.core.management.base import BaseCommand
from inventory.models import Car
from django_ledger.models import EntityModel,InvoiceModel,ItemModel
from inventory.utils import CarFinanceCalculator
from rich import print
class Command(BaseCommand):
help = ""
def handle(self, *args, **options):
e = EntityModel.objects.first()
customer = e.get_customers().first()
admin = e.admin
# estimate = e.get_estimates().first()
# e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30")
i=InvoiceModel.objects.first()
calc = CarFinanceCalculator(i)
data = calc.get_finance_data()
for car_data in data['cars']:
car = i.get_itemtxs_data()[0].filter(
item_model__car__vin=car_data['vin']
).first().item_model.car
print("car", car)
qty = Decimal(car_data['quantity'])
print("qty", qty)
# amounts from calculator
net_car_price = Decimal(car_data['total']) # after discount
net_add_price = Decimal(data['total_additionals']) # per car or split however you want
vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty
# grand_total = net_car_price + net_add_price + vat_amount
grand_total = Decimal(data['grand_total'])
cost_total = Decimal(car_data['cost_price']) * qty
print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total)
# acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)")
# acc_sales = e.get_coa_accounts().get(name="Car Sales")
# acc_tax = e.get_coa_accounts().get(name="Tax-Payable")
# acc_service = e.get_coa_accounts().get(name="After-Sales Services")
# uom = e.get_uom_all().get(name="Unit")
# car_item = e.get_items_products().get(name='2025 Ford Mustang GT')
# car_item = e.create_item_product(
# name='2025 Ford Mustang GT',
# item_type=ItemModel.ITEM_TYPE_MATERIAL,
# uom_model=uom,
# coa_model=e.get_default_coa(),
# )
# car_item.earnings_account=acc_sales
# car_item.save()
# service_item = e.get_items_services().get(name='Extended Warranty 5yr/60k')
# service_item = e.create_item_service(
# name='Extended Warranty 5yr/60k',
# uom_model=uom,
# coa_model=e.get_default_coa(),
# )
# service_item.earnings_account=acc_service
# service_item.save()
# i.invoice_items.add(car_item)
# i.invoice_items.add(service_item)
# invoices_item_models = i.invoice_items.all()
# invoice_itemtxs = {
# im.item_number: {
# 'unit_cost': Decimal(1500),
# 'unit_revenue': Decimal(1500),
# 'quantity': 1,
# 'total_amount': Decimal(1500),
# } for im in invoices_item_models
# }
# print(invoice_itemtxs)
# invoice_itemtxs = i.migrate_itemtxs(itemtxs=invoice_itemtxs,
# commit=True,
# operation=InvoiceModel.ITEMIZE_APPEND)
# print(i.amount_due)
# i.save()

View File

@ -0,0 +1,117 @@
# <your_app>/management/commands/seed_dealership.py
import json, random, string, decimal
from django.core.management.base import BaseCommand
from django.test import Client
from django.contrib.auth import get_user_model
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
from inventory.tasks import create_user_dealer
from inventory import models # adjust import to your app
from django_q.tasks import async_task
User = get_user_model()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
def add_arguments(self, parser):
parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
def handle(self, *args, **opts):
count = opts['count']
client = Client() # lives inside management command
for n in range(1, 10):
self.stdout.write(f"🚗 Seeding dealer #{n}")
self._create_dealer(client, n)
# self._create_cars(client, n)
# self._create_customers_and_sales(client, n)
self.stdout.write(self.style.SUCCESS(f"✅ Dealer #{n} ready"))
# ----------------------------------------------------------
# 1. Sign-up via the real view
# ----------------------------------------------------------
def _create_dealer(self, client, n):
payload = {
"email": f"dealer{n}@example.com",
"password": "Password123",
"confirm_password": "Password123",
"name": f"Dealer #{n}",
"arabic_name": f"تاجر {n}",
"phone_number": "+96651234567",
"crn": f"CRN{n}000",
"vrn": f"VRN{n}000",
"address": f"Street {n}, Riyadh",
}
dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address'])
user = dealer.user
self._assign_random_plan(user)
self._services(dealer)
# resp = client.post(
# "/en/signup/", # adjust URL if necessary
# data=json.dumps(payload),
# content_type="application/json",
# )
# if resp.status_code != 200:
# raise Exception(f"Signup failed: {resp.content}")
# # Log in
client.login(email=payload["email"], password=payload["password"])
return payload["email"]
def _assign_random_plan(self,user):
"""
Pick a random Plan and create + initialize a UserPlan for the user.
"""
plans = Plan.objects.all()
if not plans.exists():
raise ValueError("No plans found please create at least one Plan record.")
plan = random.choice(plans)
user_plan, created = UserPlan.objects.get_or_create(
user=user,
defaults={'plan': plan, 'active': True}
)
if created:
user_plan.initialize()
return user_plan
def _services(self,dealer):
additional_services = [
{
"name": "Vehicle registration transfer assistance",
"arabic_name": "مساعدة في نقل ملكية السيارة",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 1",
},
{
"name": "Paperwork collection",
"arabic_name": "جمع الأوراق",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 2",
},
{
"name": "Inspection and test drives",
"arabic_name": "فحص وقيادة تجريبية",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 3",
},
{
"name": "Shipping and transportation",
"arabic_name": "شحن ونقل",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 4",
},
]
for additional_service in additional_services:
models.AdditionalServices.objects.create(
name=additional_service["name"],
arabic_name=additional_service["arabic_name"],
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
)

View File

@ -0,0 +1,242 @@
import datetime
from time import sleep
import json, random, string, decimal
from django.core.management.base import BaseCommand
from django.test import Client
from django.contrib.auth import get_user_model
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
from inventory.services import decodevin
from inventory.tasks import create_user_dealer
from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff
from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel
from django_q.tasks import async_task
from faker import Faker
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
User = get_user_model()
fake = Faker()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
def add_arguments(self, parser):
parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
def handle(self, *args, **opts):
dealers = Dealer.objects.all()
for dealer in dealers:
self._create_random_po(dealer)
self._create_random_vendors(dealer)
# self._create_random_staff(dealer)
# self._create_random_cars(dealer)
self._create_random_customers(dealer)
# self._create_randome_services(dealer)
# dealer = Dealer.objects.get(name="Dealer #6")
# coa_model = dealer.entity.get_default_coa()
# inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)")
# uom = dealer.entity.get_uom_all().get(name="Unit")
# item = dealer.entity.create_item_inventory(coa_model=coa_model,inventory_account=inventory_account,uom_model=uom,item_type=ItemModel.ITEM_TYPE_MATERIAL, name=f"Test item {random.randint(1,9999)}")
# item = ItemTransactionModel.objects.create(item_name=f"Test item {random.randint(1,9999)}", entity=dealer.entity)
# po = PurchaseOrderModel.objects.first()
self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}"))
def _create_random_po(self, dealer):
for i in range(random.randint(1,70)):
try:
e: EntityModel = dealer.entity
e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}")
except Exception as e:
pass
def _create_random_vendors(self, dealer):
for i in range(random.randint(1,50)):
try:
name = fake.name()
n = random.randint(1,9999)
phone = f"05678{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}"
Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}")
except Exception as e:
pass
def _create_random_staff(self, dealer):
for i in range(5):
name = f"{fake.name()}{i}"
email = fake.email()
password = f"{fake.password()}{i}"
user = User.objects.create_user(username=email, email=email, password=password)
user.is_staff = True
user.save()
staff_member = StaffMember.objects.create(user=user)
services = Service.objects.all()
for service in services:
staff_member.services_offered.add(service)
staff = Staff.objects.create(dealer=dealer,staff_member=staff_member,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
groups = CustomGroup.objects.filter(dealer=dealer)
random_group = random.choice(list(groups))
staff.add_group(random_group.group)
# for i in range(random.randint(1,15)):
# n = random.randint(1,9999)
# phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}"
# Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}")
def _create_random_cars(self,dealer):
vendors = Vendor.objects.filter(dealer=dealer).all()
vin_list = [
"1B3ES56C13D120225",
"1GB4KYC86FF131536",
"1HSHXAHR15J136217",
"1G1ZT52845F231124",
"1J4GK48K43W721617",
"JTDBE32K430163717",
"1J4FA69S05P331572",
"2FMGK5D86EBD28496",
"KNADE243696530337",
"1N4AL21EX8N499928",
"1N4AL21E49N400571",
"1G2NW12E54C145398",
]
for vin in vin_list:
try:
for _ in range(random.randint(1,2)):
vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
result = decodevin(vin)
make = CarMake.objects.get(name=result["maker"])
model = make.carmodel_set.filter(name__contains=result["model"]).first()
if not model or model == "":
model = random.choice(make.carmodel_set.all())
year = result["modelYear"]
serie = random.choice(model.carserie_set.all())
trim = random.choice(serie.cartrim_set.all())
vendor = random.choice(vendors)
print(make, model, serie, trim, vendor,vin)
car = Car.objects.create(
vin=vin,
id_car_make=make,
id_car_model=model,
id_car_serie=serie,
id_car_trim=trim,
vendor=vendor,
year=(int(year) or 2025),
receiving_date=datetime.datetime.now(),
dealer=dealer,
mileage=0,
)
print(car)
CarFinance.objects.create(
car=car, cost_price=random.randint(10000, 100000), selling_price=0,marked_price=random.randint(10000, 100000)+random.randint(2000, 7000)
)
CarColors.objects.create(
car=car,
interior=random.choice(InteriorColors.objects.all()),
exterior=random.choice(ExteriorColors.objects.all()),
)
print(make, model, serie, trim)
except Exception as e:
print(e)
def _create_random_customers(self,dealer):
for i in range(random.randint(1,60)):
try:
c = Customer(
dealer=dealer,
title="MR",
first_name=fake.name(),
last_name=fake.last_name(),
gender="m",
email=fake.email(),
phone_number=fake.phone_number(),
address=fake.address(),
national_id=random.randint(1000000000, 9999999999),
)
c.create_user_model()
c.create_customer_model()
c.save()
except Exception as e:
pass
def _create_randome_services(self,dealer):
additional_services = [
{
"name": "Vehicle registration transfer assistance",
"arabic_name": "مساعدة في نقل ملكية السيارة",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 1",
},
{
"name": "Paperwork collection",
"arabic_name": "جمع الأوراق",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 2",
},
{
"name": "Inspection and test drives",
"arabic_name": "فحص وقيادة تجريبية",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 3",
},
{
"name": "Shipping and transportation",
"arabic_name": "شحن ونقل",
"price": decimal.Decimal(random.randrange(100, 1000)),
"description": "This is service 4",
},
]
uom = UnitOfMeasure.EACH
for additional_service in additional_services:
AdditionalServices.objects.create(
name=additional_service["name"],
arabic_name=additional_service["arabic_name"],
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
uom=uom
)
def _create_random_lead(self,dealer):
for i in range(random.randint(1,60)):
try:
first_name = fake.name()
last_name = fake.last_name()
email = fake.email()
staff = random.choice(Staff.objects.filter(dealer=dealer))
make = random.choice(CarMake.objects.all())
model = random.choice(make.carmodel_set.all())
lead = Lead.objects.create(
dealer=dealer,
first_name=first_name,
last_name=last_name,
email=email,
address=fake.address(),
lead_type="customer",
id_car_make=make,
id_car_model=model,
source="website",
channel="website",
staff=staff
)
c = Customer(
dealer=dealer,
title="MR",
first_name=fake.name(),
last_name=fake.last_name(),
gender="m",
email=fake.email(),
phone_number=fake.phone_number(),
address=fake.address(),
national_id=random.randint(1000000000, 9999999999),
)
c.create_user_model()
c.create_customer_model()
c.save()
except Exception as e:
pass

View File

@ -1,4 +1,5 @@
import logging import logging
import time
# from django.http import Http404, HttpResponseForbidden # from django.http import Http404, HttpResponseForbidden
# from django.shortcuts import redirect # from django.shortcuts import redirect
@ -44,33 +45,33 @@ logger = logging.getLogger("user_activity")
# return request.META.get("REMOTE_ADDR") # return request.META.get("REMOTE_ADDR")
class InjectParamsMiddleware: # class InjectParamsMiddleware:
""" # """
Middleware to add processed user-related parameters to the request object. # Middleware to add processed user-related parameters to the request object.
This middleware processes incoming requests to extract and enhance user # This middleware processes incoming requests to extract and enhance user
information, specifically linking user context such as `dealer` to the # information, specifically linking user context such as `dealer` to the
request. It allows subsequent views and middlewares to access these enriched # request. It allows subsequent views and middlewares to access these enriched
request parameters with ease. # request parameters with ease.
:ivar get_response: The callable to get the next middleware or view response. # :ivar get_response: The callable to get the next middleware or view response.
:type get_response: Callable # :type get_response: Callable
""" # """
def __init__(self, get_response): # def __init__(self, get_response):
self.get_response = get_response # self.get_response = get_response
def __call__(self, request): # def __call__(self, request):
try: # try:
if request.user.is_authenticated: # if request.user.is_authenticated:
request.dealer = get_user_type(request) # request.dealer = get_user_type(request)
request.entity = request.dealer.entity # request.entity = request.dealer.entity
else: # else:
request.dealer = None # request.dealer = None
except Exception: # except Exception:
pass # pass
response = self.get_response(request) # response = self.get_response(request)
return response # return response
class InjectDealerMiddleware: class InjectDealerMiddleware:
@ -93,6 +94,7 @@ class InjectDealerMiddleware:
def __call__(self, request): def __call__(self, request):
try: try:
start = time.time()
if request.user.is_authenticated: if request.user.is_authenticated:
request.is_dealer = False request.is_dealer = False
request.is_staff = False request.is_staff = False
@ -103,6 +105,7 @@ class InjectDealerMiddleware:
if hasattr(request.user, "dealer"): if hasattr(request.user, "dealer"):
request.is_dealer = True request.is_dealer = True
request.dealer = request.user.dealer request.dealer = request.user.dealer
elif hasattr(request.user, "staffmember"): elif hasattr(request.user, "staffmember"):
request.is_staff = True request.is_staff = True
request.staff = request.user.staffmember.staff request.staff = request.user.staffmember.staff
@ -120,6 +123,7 @@ class InjectDealerMiddleware:
request.is_inventory = True request.is_inventory = True
request.entity = request.dealer.entity request.entity = request.dealer.entity
request.admin = request.dealer.entity.admin request.admin = request.dealer.entity.admin
print("\033[92m⏱ Middleware time:", time.time() - start, "\033[0m")
except Exception: except Exception:
pass pass
response = self.get_response(request) response = self.get_response(request)

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-07-15 11:38 # Generated by Django 5.2.4 on 2025-07-22 08:37
import datetime import datetime
import django.core.serializers.json import django.core.serializers.json
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
('name', models.CharField(blank=True, max_length=255, null=True)), ('name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')), ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)), ('is_sa_import', models.BooleanField(default=False)),
('car_type', models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True)), ('car_type', models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True)),
], ],
@ -253,7 +253,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=255, verbose_name='English Name')), ('name', models.CharField(max_length=255, verbose_name='English Name')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, default='logo.png', null=True, upload_to='logos/users', verbose_name='Logo')), ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/users', verbose_name='Logo')),
('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')), ('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
@ -566,7 +566,7 @@ class Migration(migrations.Migration):
('email', models.EmailField(max_length=254, verbose_name='Email')), ('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')), ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos', verbose_name='Logo')),
('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')),
@ -772,7 +772,7 @@ class Migration(migrations.Migration):
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')), ('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/staff', verbose_name='Image')), ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/staff', verbose_name='Image')),
('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')),
@ -872,7 +872,7 @@ class Migration(migrations.Migration):
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('email', models.EmailField(max_length=255, verbose_name='Email Address')), ('email', models.EmailField(max_length=255, verbose_name='Email Address')),
('address', models.CharField(max_length=200, verbose_name='Address')), ('address', models.CharField(max_length=200, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')), ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/vendors', verbose_name='Logo')),
('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(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')),

View File

@ -223,7 +223,7 @@ class CarMake(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.png")
is_sa_import = models.BooleanField(default=False) is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
@ -719,6 +719,12 @@ class Car(Base):
return active_reservations.exists() return active_reservations.exists()
@property @property
def logo(self):
return getattr(self.id_car_make, "logo", "")
@property
def additional_services(self):
return self.finances.additional_services.all()
@property
def ready(self): def ready(self):
try: try:
return all( return all(
@ -1174,7 +1180,13 @@ class Dealer(models.Model, LocalizedNameMixin):
blank=True, blank=True,
null=True, null=True,
verbose_name=_("Logo"), verbose_name=_("Logo"),
default="logo.png", default="user-logo.png",
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
) )
entity = models.ForeignKey( entity = models.ForeignKey(
EntityModel, on_delete=models.SET_NULL, null=True, blank=True EntityModel, on_delete=models.SET_NULL, null=True, blank=True
@ -1272,7 +1284,7 @@ class Staff(models.Model, LocalizedNameMixin):
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
logo = models.ImageField( logo = models.ImageField(
upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image") upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="user-logo.png"
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1497,6 +1509,12 @@ class Customer(models.Model):
image = models.ImageField( image = models.ImageField(
upload_to="customers/", blank=True, null=True, verbose_name=_("Image") upload_to="customers/", blank=True, null=True, verbose_name=_("Image")
) )
thumbnail = ImageSpecField(
source="image",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
)
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(
@ -1639,7 +1657,13 @@ class Organization(models.Model, LocalizedNameMixin):
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
logo = models.ImageField( logo = models.ImageField(
upload_to="logos", blank=True, null=True, verbose_name=_("Logo") upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="user-logo.png"
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
) )
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"))
@ -2453,7 +2477,13 @@ class Vendor(models.Model, LocalizedNameMixin):
email = models.EmailField(max_length=255, verbose_name=_("Email Address")) email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address")) address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField( logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="user-logo.png"
)
thumbnail = ImageSpecField(
source="logo",
processors=[ResizeToFill(40, 40)],
format="WEBP",
options={"quality": 80},
) )
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"))
@ -2776,7 +2806,7 @@ class SaleOrder(models.Model):
if self.invoice: if self.invoice:
# Check if get_itemtxs_data returns data before proceeding # Check if get_itemtxs_data returns data before proceeding
# You might want to handle what get_itemtxs_data returns if it can be empty # You might want to handle what get_itemtxs_data returns if it can be empty
item_data = self.invoice.get_itemtxs_data() item_data = self.estimate.get_itemtxs_data()[0]
if item_data: if item_data:
return item_data return item_data
return [] # Return an empty list if no invoice or no item data return [] # Return an empty list if no invoice or no item data
@ -3301,7 +3331,7 @@ class ExtraInfo(models.Model):
return f"ExtraInfo for {self.content_object} ({self.content_type})" return f"ExtraInfo for {self.content_object} ({self.content_type})"
@classmethod @classmethod
def get_sale_orders(cls, staff=None, is_dealer=False): def get_sale_orders(cls, staff=None, is_dealer=False,dealer=None):
if not staff and not is_dealer: if not staff and not is_dealer:
return [] return []
@ -3310,12 +3340,18 @@ class ExtraInfo(models.Model):
if is_dealer: if is_dealer:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer,
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id__isnull=False, related_object_id__isnull=False,
) ).union(cls.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(User),
))
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer,
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id=staff.pk, related_object_id=staff.pk,
@ -3330,7 +3366,7 @@ class ExtraInfo(models.Model):
] ]
@classmethod @classmethod
def get_invoices(cls, staff=None, is_dealer=False): def get_invoices(cls, staff=None, is_dealer=False,dealer=None):
if not staff and not is_dealer: if not staff and not is_dealer:
return [] return []
@ -3339,17 +3375,22 @@ class ExtraInfo(models.Model):
if is_dealer: if is_dealer:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer,
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id__isnull=False, related_object_id__isnull=False,
) ).union(cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=ContentType.objects.get_for_model(User),
))
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer,
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id=staff.pk, related_object_id=staff.pk,
) )
print(qs[0].content_object.invoicemodel_set.first())
return [ return [
x.content_object.invoicemodel_set.first() x.content_object.invoicemodel_set.first()
for x in qs for x in qs

View File

View File

@ -0,0 +1,205 @@
# import json
# from django.contrib.auth.models import AnonymousUser
# from django.contrib.auth import get_user_model
# from django.db import close_old_connections
# from urllib.parse import parse_qs
# from channels.db import database_sync_to_async
# from inventory.models import Notification
# import asyncio
# @database_sync_to_async
# def get_notifications(user, last_id):
# return Notification.objects.filter(
# user=user, id__gt=last_id, is_read=False
# ).order_by("created")
# class NotificationSSEApp:
# async def __call__(self, scope, receive, send):
# if scope["type"] != "http":
# return
# query_string = parse_qs(scope["query_string"].decode())
# last_id = int(query_string.get("last_id", [0])[0])
# # Get user from scope if using AuthMiddlewareStack
# user = scope.get("user", AnonymousUser())
# if not user.is_authenticated:
# await send({
# "type": "http.response.start",
# "status": 403,
# "headers": [(b"content-type", b"text/plain")],
# })
# await send({
# "type": "http.response.body",
# "body": b"Unauthorized",
# })
# return
# await send({
# "type": "http.response.start",
# "status": 200,
# "headers": [
# (b"content-type", b"text/event-stream"),
# (b"cache-control", b"no-cache"),
# (b"x-accel-buffering", b"no"),
# ]
# })
# try:
# while True:
# close_old_connections()
# notifications = await get_notifications(user, last_id)
# for notification in notifications:
# data = {
# "id": notification.id,
# "message": notification.message,
# "created": notification.created.isoformat(),
# "is_read": notification.is_read,
# }
# event_str = (
# f"id: {notification.id}\n"
# f"event: notification\n"
# f"data: {json.dumps(data)}\n\n"
# )
# await send({
# "type": "http.response.body",
# "body": event_str.encode("utf-8"),
# "more_body": True
# })
# last_id = notification.id
# await asyncio.sleep(2)
# except asyncio.CancelledError:
# pass
import json
import time
from django.contrib.auth.models import AnonymousUser
from urllib.parse import parse_qs
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from inventory.models import Notification
import asyncio
from datetime import datetime
@database_sync_to_async
def get_user(user_id):
User = get_user_model()
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
@database_sync_to_async
def get_notifications(user, last_id):
notifications = Notification.objects.filter(
user=user,
id__gt=last_id,
is_read=False
).order_by("created")
return [
{
'id': n.id,
'message': n.message,
'created': n.created.isoformat(), # Convert datetime to string
'is_read': n.is_read
}
for n in notifications
]
class NotificationSSEApp:
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
return
# Parse query parameters
query_string = parse_qs(scope["query_string"].decode())
last_id = int(query_string.get("last_id", [0])[0])
# Get user from scope
user = scope.get("user")
if not user or user.is_anonymous:
await self._send_response(send, 403, b"Unauthorized")
return
# Send SSE headers
await self._send_headers(send)
try:
while True:
try:
message = await asyncio.wait_for(receive(), timeout=3)
if message["type"] == "http.disconnect":
print("🔌 Client disconnected")
break
except asyncio.TimeoutError:
notifications = await get_notifications(user, last_id)
for notification in notifications:
await self._send_notification(send, notification)
if notification['id'] > last_id:
last_id = notification['id']
# Send keep-alive comment every 15 seconds
await send({
"type": "http.response.body",
"body": b":keep-alive\n\n",
"more_body": True
})
# await asyncio.sleep(3)
except (asyncio.CancelledError, ConnectionResetError):
pass
finally:
await self._close_connection(send)
async def _send_headers(self, send):
await send({
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"text/event-stream"),
(b"cache-control", b"no-cache"),
(b"connection", b"keep-alive"),
(b"x-accel-buffering", b"no"),
]
})
async def _send_notification(self, send, notification):
try:
event_str = (
f"id: {notification['id']}\n"
f"event: notification\n"
f"data: {json.dumps(notification)}\n\n"
)
await send({
"type": "http.response.body",
"body": event_str.encode("utf-8"),
"more_body": True
})
except Exception as e:
print(f"Error sending notification: {e}")
async def _send_response(self, send, status, body):
await send({
"type": "http.response.start",
"status": status,
"headers": [(b"content-type", b"text/plain")]
})
await send({
"type": "http.response.body",
"body": body
})
async def _close_connection(self, send):
await send({
"type": "http.response.body",
"body": b""
})

View File

@ -1,3 +1,4 @@
from datetime import timezone
import logging import logging
from .models import Dealer from .models import Dealer
from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.exceptions import ImproperlyConfigured, ValidationError
@ -19,7 +20,7 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django_ledger.models import ItemTransactionModel from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django_ledger.forms.purchase_order import ( from django_ledger.forms.purchase_order import (
ApprovedPurchaseOrderModelUpdateForm, ApprovedPurchaseOrderModelUpdateForm,
@ -35,6 +36,11 @@ from django.views.generic.edit import UpdateView
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm,
get_invoice_itemtxs_formset_class,
DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm,
AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -315,41 +321,68 @@ class BasePurchaseOrderActionActionView(
f"User {user_username} attempting to call action '{self.action_name}' " f"User {user_username} attempting to call action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
) )
try: print(self.action_name)
getattr(po_model, self.action_name)(commit=self.commit, **kwargs) if self.action_name == "mark_as_fulfilled":
# --- Single-line log for successful action --- try:
logger.info( if po_model.can_fulfill():
f"User {user_username} successfully executed action '{self.action_name}' " po_model.mark_as_fulfilled()
f"on Purchase Order ID: {po_model.pk}." # po_model.date_fulfilled = timezone.now()
) po_model.save()
messages.add_message( messages.add_message(
request, request,
message="PO updated successfully.", message="PO marked as fulfilled successfully.",
level=messages.SUCCESS, level=messages.SUCCESS,
) )
except ValidationError as e: logger.info(
# --- Single-line log for ValidationError --- f"User {user_username} successfully executed action '{self.action_name}' "
print( f"on Purchase Order ID: {po_model.pk}."
f"User {user_username} encountered a validation error " )
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " except Exception as e:
f"Error: {e}" messages.add_message(
) request,
logger.warning( message=f"Failed to mark PO {po_model.po_number} as fulfilled. {e}",
f"User {user_username} encountered a validation error " level=messages.ERROR,
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " )
f"Error: {e}" logger.warning(
) f"User {user_username} encountered an exception "
except AttributeError as e: f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
print( f"Error: {e}"
f"User {user_username} encountered an AttributeError " )
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " else:
f"Error: {e}" try:
) getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
logger.warning( logger.info(
f"User {user_username} encountered an AttributeError " f"User {user_username} successfully executed action '{self.action_name}' "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " f"on Purchase Order ID: {po_model.pk}."
f"Error: {e}" )
) messages.add_message(
request,
message="PO updated successfully.",
level=messages.SUCCESS,
)
except ValidationError as e:
# --- Single-line log for ValidationError ---
print(
f"User {user_username} encountered a validation error "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
logger.warning(
f"User {user_username} encountered a validation error "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
except AttributeError as e:
print(
f"User {user_username} encountered an AttributeError "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
logger.warning(
f"User {user_username} encountered an AttributeError "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
return response return response
@ -731,3 +764,212 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
entity_slug=self.kwargs["entity_slug"], entity_slug=self.kwargs["entity_slug"],
) )
return super().get_queryset() return super().get_queryset()
class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
slug_url_kwarg = 'invoice_pk'
slug_field = 'uuid'
context_object_name = 'invoice'
# template_name = 'inventory/sales/invoices/invoice_update.html'
form_class = BaseInvoiceModelUpdateForm
http_method_names = ['get', 'post']
action_update_items = False
def get_form_class(self):
invoice_model: InvoiceModel = self.object
if invoice_model.is_draft():
return DraftInvoiceModelUpdateForm
elif invoice_model.is_review():
return InReviewInvoiceModelUpdateForm
elif invoice_model.is_approved():
if invoice_model.accrue:
return AccruedAndApprovedInvoiceModelUpdateForm
return ApprovedInvoiceModelUpdateForm
elif invoice_model.is_paid():
return PaidInvoiceModelUpdateForm
return BaseInvoiceModelUpdateForm
def get_form(self, form_class=None):
form_class = self.get_form_class()
if self.request.method == 'POST' and self.action_update_items:
return form_class(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.dealer.user,
instance=self.object
)
return form_class(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.dealer.user,
**self.get_form_kwargs()
)
def get_context_data(self, itemtxs_formset=None, **kwargs):
context = super().get_context_data(**kwargs)
invoice_model: InvoiceModel = self.object
title = f'Invoice {invoice_model.invoice_number}'
context['page_title'] = title
context['header_title'] = title
ledger_model: LedgerModel = self.object.ledger
if not invoice_model.is_configured():
messages.add_message(
request=self.request,
message=f'Invoice {invoice_model.invoice_number} must have all accounts configured.',
level=messages.ERROR,
extra_tags='is-danger'
)
if not invoice_model.is_paid():
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This invoice is locked. Must unlock before making any changes.',
extra_tags='is-danger')
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This Invoice is Locked. Must unlock before making any changes.',
extra_tags='is-danger')
if not ledger_model.is_posted():
messages.add_message(self.request,
messages.INFO,
f'This Invoice has not been posted. Must post to see ledger changes.',
extra_tags='is-info')
if not itemtxs_formset:
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model')
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs)
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
itemtxs_formset = invoice_itemtxs_formset_class(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.dealer.user,
invoice_model=invoice_model,
queryset=itemtxs_qs
)
else:
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset)
context['itemtxs_formset'] = itemtxs_formset
context['total_amount__sum'] = itemtxs_agg['total_amount__sum']
return context
def get_success_url(self):
entity_slug = self.kwargs['entity_slug']
invoice_pk = self.kwargs['invoice_pk']
return reverse('invoice_detail',
kwargs={
'dealer_slug': self.request.dealer.slug,
'entity_slug': entity_slug,
'pk': invoice_pk
})
# def get_queryset(self):
# qs = super().get_queryset()
# return qs.prefetch_related('itemtransactionmodel_set')
def get_queryset(self):
if self.queryset is None:
self.queryset = InvoiceModel.objects.for_entity(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user
).select_related('customer', 'ledger').order_by('-created')
return super().get_queryset().prefetch_related('itemtransactionmodel_set')
def form_valid(self, form):
invoice_model: InvoiceModel = form.save(commit=False)
if invoice_model.can_migrate():
invoice_model.migrate_state(
user_model=self.request.dealer.user,
entity_slug=self.kwargs['entity_slug']
)
messages.add_message(self.request,
messages.SUCCESS,
f'Invoice {self.object.invoice_number} successfully updated.',
extra_tags='is-success')
return super().form_valid(form)
def get(self, request, entity_slug, invoice_pk, *args, **kwargs):
if self.action_update_items:
return HttpResponseRedirect(
redirect_to=reverse('invoice_update',
kwargs={
'dealer_slug': request.dealer.slug,
'entity_slug': entity_slug,
'pk': invoice_pk
})
)
return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs)
def post(self, request, entity_slug, invoice_pk, *args, **kwargs):
if self.action_update_items:
if not request.user.is_authenticated:
return HttpResponseForbidden()
queryset = self.get_queryset()
invoice_model = self.get_object(queryset=queryset)
self.object = invoice_model
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
itemtxs_formset = invoice_itemtxs_formset_class(request.POST,
user_model=self.request.dealer.user,
invoice_model=invoice_model,
entity_slug=entity_slug)
if not invoice_model.can_edit_items():
messages.add_message(
request,
message=f'Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}',
level=messages.ERROR,
extra_tags='is-danger'
)
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
return self.render_to_response(context=context)
if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False)
entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user)
entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug)
for itemtxs in itemtxs_list:
itemtxs.invoice_model_id = invoice_model.uuid
itemtxs.clean()
itemtxs_list = itemtxs_formset.save()
itemtxs_qs = invoice_model.update_amount_due()
invoice_model.get_state(commit=True)
invoice_model.clean()
invoice_model.save(
update_fields=['amount_due',
'amount_receivable',
'amount_unearned',
'amount_earned',
'updated']
)
invoice_model.migrate_state(
entity_slug=entity_slug,
user_model=self.request.user,
raise_exception=False,
itemtxs_qs=itemtxs_qs
)
messages.add_message(request,
message=f'Items for Invoice {invoice_model.invoice_number} saved.',
level=messages.SUCCESS,
extra_tags='is-success')
return HttpResponseRedirect(
redirect_to=reverse('django_ledger:invoice-update',
kwargs={
'entity_slug': entity_slug,
'invoice_pk': invoice_pk
})
)
# if not valid, return formset with errors...
return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset))
return super(InvoiceModelUpdateView, self).post(request, **kwargs)

View File

@ -264,18 +264,19 @@ def create_item_model(sender, instance, created, **kwargs):
uom_model=uom, uom_model=uom,
coa_model=coa, coa_model=coa,
) )
instance.item_model = inventory
inventory.save()
# inventory = entity.create_item_inventory( # inventory = entity.create_item_inventory(
# name=instance.vin, # name=instance.vin,
# uom_model=uom, # uom_model=uom,
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM # item_type=ItemModel.ITEM_TYPE_LUMP_SUM
# ) # )
instance.item_model = inventory # inventory.additional_info = {}
inventory.additional_info = {} # inventory.additional_info.update({"car_info": instance.to_dict()})
inventory.additional_info.update({"car_info": instance.to_dict()}) # inventory.save()
inventory.save() # else:
else: # instance.item_model.additional_info.update({"car_info": instance.to_dict()})
instance.item_model.additional_info.update({"car_info": instance.to_dict()}) # instance.item_model.save()
instance.item_model.save()
# # update price - CarFinance # # update price - CarFinance
@ -370,14 +371,14 @@ def update_item_model_cost(sender, instance, created, **kwargs):
instance.car.item_model.default_amount = instance.marked_price instance.car.item_model.default_amount = instance.marked_price
if not isinstance(instance.car.item_model.additional_info, dict): if not isinstance(instance.car.item_model.additional_info, dict):
instance.car.item_model.additional_info = {} instance.car.item_model.additional_info = {}
instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()}) # instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
instance.car.item_model.additional_info.update( # instance.car.item_model.additional_info.update(
{ # {
"additional_services": [ # "additional_services": [
service.to_dict() for service in instance.additional_services.all() # service.to_dict() for service in instance.additional_services.all()
] # ]
} # }
) # )
instance.car.item_model.save() instance.car.item_model.save()
print(f"Inventory item updated with CarFinance data for Car: {instance.car}") print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
@ -959,24 +960,24 @@ def add_service_to_staff(sender, instance, created, **kwargs):
########################################################## ##########################################################
@receiver(post_save, sender=PurchaseOrderModel) # @receiver(post_save, sender=PurchaseOrderModel)
def create_po_fulfilled_notification(sender, instance, created, **kwargs): # def create_po_fulfilled_notification(sender, instance, created, **kwargs):
if instance.po_status == "fulfilled": # if instance.po_status == "fulfilled":
dealer = models.Dealer.objects.get(entity=instance.entity) # dealer = models.Dealer.objects.get(entity=instance.entity)
accountants = ( # accountants = (
models.CustomGroup.objects.filter(dealer=dealer, name="Inventory") # models.CustomGroup.objects.filter(dealer=dealer, name="Inventory")
.first() # .first()
.group.user_set.exclude(email=dealer.user.email) # .group.user_set.exclude(email=dealer.user.email)
.distinct() # .distinct()
) # )
for accountant in accountants: # for accountant in accountants:
models.Notification.objects.create( # models.Notification.objects.create(
user=accountant, # user=accountant,
message=f""" # message=f"""
New Purchase Order {instance.po_number} has been added to dealer {dealer.name}. # New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a> # <a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", # """,
) # )
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
@ -1005,7 +1006,7 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled(): if instance.is_fulfilled():
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = User.objects.filter( recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer, groups__customgroup__dealer=dealer,
groups__customgroup__name__in=["Manager", "Inventory"], groups__customgroup__name__in=["Manager", "Inventory"],
).distinct() ).distinct()
for recipient in recipients: for recipient in recipients:
@ -1099,7 +1100,8 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs):
related_content_type=ContentType.objects.get_for_model(models.Staff), related_content_type=ContentType.objects.get_for_model(models.Staff),
object_id=instance.pk, object_id=instance.pk,
).first() ).first()
if not recipient:
return
models.Notification.objects.create( models.Notification.objects.create(
user=recipient.related_object.user, user=recipient.related_object.user,
message=f""" message=f"""

View File

@ -4,9 +4,10 @@ from django_ledger.io import roles
from django_q.tasks import async_task from django_q.tasks import async_task
from django.core.mail import send_mail from django.core.mail import send_mail
from appointment.models import StaffMember from appointment.models import StaffMember
from django.contrib.auth.models import User, Group, Permission from allauth.account.models import EmailAddress
from inventory.models import DealerSettings, Dealer from inventory.models import DealerSettings, Dealer
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -220,7 +221,7 @@ def create_coa_accounts(instance):
"role": roles.LIABILITY_CL_TAXES_PAYABLE, "role": roles.LIABILITY_CL_TAXES_PAYABLE,
"balance_type": roles.CREDIT, "balance_type": roles.CREDIT,
"locked": False, "locked": False,
"default": True, # Default for LIABILITY_CL_TAXES_PAYABLE "default": False, # Default for LIABILITY_CL_TAXES_PAYABLE
}, },
{ {
"code": "2070", "code": "2070",
@ -239,6 +240,14 @@ def create_coa_accounts(instance):
"locked": False, "locked": False,
"default": True, # Default for LIABILITY_CL_DEFERRED_REVENUE "default": True, # Default for LIABILITY_CL_DEFERRED_REVENUE
}, },
{
"code": "2200",
"name": "Tax Payable",
"role": roles.LIABILITY_CL_TAXES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True,
},
{ {
"code": "2210", "code": "2210",
"name": "Long-term Bank Loans", "name": "Long-term Bank Loans",
@ -1141,6 +1150,15 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
user = User.objects.create(username=email, email=email) user = User.objects.create(username=email, email=email)
user.set_password(password) user.set_password(password)
user.save() user.save()
#TODO remove this later
EmailAddress.objects.create(
user=user,
email=user.email,
verified=True,
primary=True
)
group = Group.objects.create(name=f"{user.pk}-Admin") group = Group.objects.create(name=f"{user.pk}-Admin")
user.groups.add(group) user.groups.add(group)
for perm in Permission.objects.filter( for perm in Permission.objects.filter(
@ -1149,7 +1167,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
group.permissions.add(perm) group.permissions.add(perm)
StaffMember.objects.create(user=user) StaffMember.objects.create(user=user)
Dealer.objects.create( dealer = Dealer.objects.create(
user=user, user=user,
name=name, name=name,
arabic_name=arabic_name, arabic_name=arabic_name,
@ -1158,6 +1176,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
phone_number=phone, phone_number=phone,
address=address, address=address,
) )
return dealer
# def create_groups(dealer_slug): # def create_groups(dealer_slug):

View File

@ -472,6 +472,7 @@ def po_item_formset_table(context, po_model, itemtxs_formset, user):
@register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True) @register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
def bill_item_formset_table(context, item_formset): def bill_item_formset_table(context, item_formset):
bill = BillModel.objects.get(uuid=context["view"].kwargs["bill_pk"])
for item in item_formset: for item in item_formset:
if item: if item:
item.initial["quantity"] = item.instance.po_quantity item.initial["quantity"] = item.instance.po_quantity
@ -484,6 +485,7 @@ def bill_item_formset_table(context, item_formset):
return { return {
"dealer_slug": context["view"].kwargs["dealer_slug"], "dealer_slug": context["view"].kwargs["dealer_slug"],
"entity_slug": context["view"].kwargs["entity_slug"], "entity_slug": context["view"].kwargs["entity_slug"],
"bill": bill,
"bill_pk": context["view"].kwargs["bill_pk"], "bill_pk": context["view"].kwargs["bill_pk"],
"total_amount__sum": context["total_amount__sum"], "total_amount__sum": context["total_amount__sum"],
"item_formset": item_formset, "item_formset": item_formset,
@ -672,3 +674,12 @@ def count_checked(permissions, group_permission_ids):
# def count_checked(permissions, group_permission_ids): # def count_checked(permissions, group_permission_ids):
# """Count how many permissions are checked from the allowed list""" # """Count how many permissions are checked from the allowed list"""
# return sum(1 for perm in permissions if perm.id in group_permission_ids) # return sum(1 for perm in permissions if perm.id in group_permission_ids)
@register.inclusion_tag('sales/tags/invoice_item_formset.html', takes_context=True)
def invoice_item_formset_table(context, itemtxs_formset):
return {
'entity_slug': context['view'].kwargs['entity_slug'],
'invoice_model': context['invoice'],
'total_amount__sum': context['total_amount__sum'],
'itemtxs_formset': itemtxs_formset,
}

View File

@ -80,11 +80,11 @@ urlpatterns = [
views.CustomerDetailView.as_view(), views.CustomerDetailView.as_view(),
name="customer_detail", name="customer_detail",
), ),
path( # path(
"<slug:dealer_slug>/customers/<slug:slug>/add-note/", # "<slug:dealer_slug>/customers/<slug:slug>/add-note/",
views.add_note_to_customer, # views.add_note_to_customer,
name="add_note_to_customer", # name="add_note_to_customer",
), # ),
path( path(
"<slug:dealer_slug>/customers/<slug:slug>/update/", "<slug:dealer_slug>/customers/<slug:slug>/update/",
views.CustomerUpdateView.as_view(), views.CustomerUpdateView.as_view(),
@ -198,11 +198,11 @@ urlpatterns = [
views.lead_transfer, views.lead_transfer,
name="lead_transfer", name="lead_transfer",
), ),
path( # path(
"<slug:dealer_slug>/crm/opportunities/<slug:slug>/add_note/", # "<slug:dealer_slug>/crm/opportunities/<slug:slug>/add_note/",
views.add_note_to_opportunity, # views.add_note_to_opportunity,
name="add_note_to_opportunity", # name="add_note_to_opportunity",
), # ),
path( path(
"<slug:dealer_slug>/crm/opportunities/create/", "<slug:dealer_slug>/crm/opportunities/create/",
views.OpportunityCreateView.as_view(), views.OpportunityCreateView.as_view(),
@ -836,10 +836,15 @@ urlpatterns = [
name="invoice_create", name="invoice_create",
), ),
path( path(
"<slug:dealer_slug>/sales/invoices/<uuid:pk>/", "<slug:dealer_slug>/sales/<slug:entity_slug>/invoices/<uuid:pk>/",
views.InvoiceDetailView.as_view(), views.InvoiceDetailView.as_view(),
name="invoice_detail", name="invoice_detail",
), ),
# path(
# "<slug:dealer_slug>/sales/<slug:entity_slug>/invoices/<uuid:pk>/update",
# views.InvoiceDetailView.as_view(),
# name="invoice_update",
# ),
path( path(
"<slug:dealer_slug>/sales/invoices/<uuid:pk>/preview/", "<slug:dealer_slug>/sales/invoices/<uuid:pk>/preview/",
views.InvoicePreviewView.as_view(), views.InvoicePreviewView.as_view(),
@ -876,7 +881,17 @@ urlpatterns = [
views.PaymentCreateView, views.PaymentCreateView,
name="payment_create", name="payment_create",
), ),
# path("sales/payments/create/", views.PaymentCreateView, name="payment_create"), # path(
# "<slug:dealer_slug>/sales/payments/<slug:entity_slug>/<uuid:invoice_pk>/create/",
# views.InvoiceModelUpdateView.as_view(),
# name="invoice_update",
# ),
# path(
# "<slug:dealer_slug>/sales/payments/<slug:entity_slug>/<uuid:invoice_pk>/create/",
# views.InvoiceModelUpdateView.as_view(),
# name="payment_create",
# ),
# path("<slug:dealer_slug>/sales/payments/create/", views.PaymentCreateView, name="payment_create"),
path( path(
"<slug:dealer_slug>/sales/payments/<uuid:pk>/payment_details/", "<slug:dealer_slug>/sales/payments/<uuid:pk>/payment_details/",
views.PaymentDetailView, views.PaymentDetailView,

View File

@ -14,6 +14,7 @@ from django_q.tasks import async_task
from django.core.mail import send_mail from django.core.mail import send_mail
from plans.models import AbstractOrder from plans.models import AbstractOrder
from django_ledger.models import ( from django_ledger.models import (
EstimateModel,
InvoiceModel, InvoiceModel,
BillModel, BillModel,
VendorModel, VendorModel,
@ -25,8 +26,9 @@ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django_ledger.models.transactions import TransactionModel from django_ledger.models.transactions import TransactionModel
from django_ledger.models.journal_entry import JournalEntryModel from django_ledger.models.journal_entry import JournalEntryModel
from django.db import transaction
import logging import logging
from django_ledger.io import roles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -452,30 +454,35 @@ def get_financial_values(model):
} }
def set_invoice_payment(dealer, entity, invoice, amount, payment_method): # def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
""" # """
Processes and applies a payment for a specified invoice. This function calculates # Processes and applies a payment for a specified invoice. This function calculates
finance details, handles associated account transactions, and updates the invoice # finance details, handles associated account transactions, and updates the invoice
status accordingly. # status accordingly.
:param dealer: Dealer object responsible for processing the payment # :param dealer: Dealer object responsible for processing the payment
:type dealer: Dealer # :type dealer: Dealer
:param entity: Entity object associated with the invoice and payment # :param entity: Entity object associated with the invoice and payment
:type entity: Entity # :type entity: Entity
:param invoice: The invoice object for which the payment is being made # :param invoice: The invoice object for which the payment is being made
:type invoice: Invoice # :type invoice: Invoice
:param amount: The amount being paid towards the invoice # :param amount: The amount being paid towards the invoice
:type amount: Decimal # :type amount: Decimal
:param payment_method: The payment method used for the transaction # :param payment_method: The payment method used for the transaction
:type payment_method: str # :type payment_method: str
:return: None # :return: None
""" # """
calculator = CarFinanceCalculator(invoice) # calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data() # finance_data = calculator.get_finance_data()
handle_account_process(invoice, amount, finance_data) # handle_account_process(invoice, amount, finance_data)
invoice.make_payment(amount) # if invoice.can_migrate():
invoice.save() # invoice.migrate_state(
# user_model=dealer.user,
# entity_slug=entity.slug
# )
# invoice.make_payment(amount)
# invoice.save()
def set_bill_payment(dealer, entity, bill, amount, payment_method): def set_bill_payment(dealer, entity, bill, amount, payment_method):
@ -996,16 +1003,25 @@ class CarFinanceCalculator:
ADDITIONAL_SERVICES_KEY = "additional_services" ADDITIONAL_SERVICES_KEY = "additional_services"
def __init__(self, model): def __init__(self, model):
self.dealer = models.Dealer.objects.get(entity=model.entity) if isinstance(model, InvoiceModel):
self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity)
self.extra_info = models.ExtraInfo.objects.get(
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(model.ce_model),
object_id=model.ce_model.pk,
)
elif isinstance(model, EstimateModel):
self.dealer = models.Dealer.objects.get(entity=model.entity)
self.extra_info = models.ExtraInfo.objects.get(
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(model),
object_id=model.pk,
)
self.model = model self.model = model
self.vat_rate = self._get_vat_rate() self.vat_rate = self._get_vat_rate()
self.item_transactions = self._get_item_transactions() self.item_transactions = self._get_item_transactions()
self.additional_services = self._get_additional_services() # self.additional_services = self._get_additional_services()
self.extra_info = models.ExtraInfo.objects.get(
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(model),
object_id=model.pk,
)
def _get_vat_rate(self): def _get_vat_rate(self):
vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
@ -1013,77 +1029,62 @@ class CarFinanceCalculator:
raise ObjectDoesNotExist("No active VAT rate found") raise ObjectDoesNotExist("No active VAT rate found")
return vat.rate return vat.rate
def _get_additional_services(self):
return [x for item in self.item_transactions
for x in item.item_model.car.additional_services
]
def _get_item_transactions(self): def _get_item_transactions(self):
return self.model.get_itemtxs_data()[0].all() return self.model.get_itemtxs_data()[0].all()
def get_items(self):
return self._get_item_transactions()
@staticmethod @staticmethod
def _get_quantity(item): def _get_quantity(item):
return item.ce_quantity or item.quantity return item.ce_quantity or item.quantity
def _get_nested_value(self, item, *keys): # def _get_nested_value(self, item, *keys):
current = item.item_model.additional_info # current = item.item_model.additional_info
for key in keys: # for key in keys:
current = current.get(key, {}) # current = current.get(key, {})
return current # return current
def _get_car_data(self, item): def _get_car_data(self, item):
quantity = self._get_quantity(item) quantity = self._get_quantity(item)
car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) car = item.item_model.car
car_info = self._get_nested_value(item, self.CAR_INFO_KEY) unit_price = Decimal(car.finances.marked_price)
unit_price = Decimal(car_finance.get("marked_price", 0))
return { return {
"item_number": item.item_model.item_number, "item_number": item.item_model.item_number,
"vin": car_info.get("vin"), "vin": car.vin, #car_info.get("vin"),
"make": car_info.get("make"), "make": car.id_car_make ,#car_info.get("make"),
"model": car_info.get("model"), "model": car.id_car_model ,#car_info.get("model"),
"year": car_info.get("year"), "year": car.year ,# car_info.get("year"),
"logo": getattr(item.item_model.car.id_car_make, "logo", ""), "logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car_info.get("trim"), "trim": car.id_car_trim ,# car_info.get("trim"),
"mileage": car_info.get("mileage"), "mileage": car.mileage ,# car_info.get("mileage"),
"cost_price": car_finance.get("cost_price"), "cost_price": car.finances.cost_price,
"selling_price": car_finance.get("selling_price"), "selling_price": car.finances.selling_price,
"marked_price": car_finance.get("marked_price"), "marked_price": car.finances.marked_price,
"discount": car_finance.get("discount_amount"), "discount": car.finances.discount_amount,
"quantity": quantity, "quantity": quantity,
"unit_price": unit_price, "unit_price": unit_price,
"total": unit_price * Decimal(quantity), "total": unit_price * Decimal(quantity),
"total_vat": car_finance.get("total_vat"), "total_vat": car.finances.total_vat,
"additional_services": self._get_nested_value( "additional_services": car.additional_services,# self._get_nested_value(
item, self.ADDITIONAL_SERVICES_KEY #item, self.ADDITIONAL_SERVICES_KEY
), #),
} }
def _get_additional_services(self):
return [
{
"name": service.get("name"),
"price": service.get("price"),
"taxable": service.get("taxable"),
"price_": service.get("price_"),
}
for item in self.item_transactions
for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY)
or []
]
def calculate_totals(self): def calculate_totals(self):
total_price = sum( total_price = sum(
Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "marked_price")) Decimal(item.item_model.car.finances.marked_price)
* int(self._get_quantity(item))
for item in self.item_transactions for item in self.item_transactions
) )
total_additionals = sum( total_additionals = sum(
Decimal(x.get("price_")) for x in self._get_additional_services() Decimal(item.price_) for item in self._get_additional_services())
)
total_discount = self.extra_info.data.get("discount", 0) total_discount = self.extra_info.data.get("discount", 0)
# total_discount = sum(
# Decimal(
# self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount")
# )
# for item in self.item_transactions
# )
total_price_discounted = total_price total_price_discounted = total_price
if total_discount: if total_discount:
total_price_discounted = total_price - Decimal(total_discount) total_price_discounted = total_price - Decimal(total_discount)
@ -1092,11 +1093,11 @@ class CarFinanceCalculator:
return { return {
"total_price_before_discount": round( "total_price_before_discount": round(
total_price, 2 total_price, 2
), # total_price_before_discount, ),
"total_price": round(total_price_discounted, 2), # total_price_discounted, "total_price": round(total_price_discounted, 2),
"total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, "total_vat_amount": round(total_vat_amount, 2),
"total_discount": round(Decimal(total_discount)), "total_discount": round(Decimal(total_discount)),
"total_additionals": round(total_additionals, 2), # total_additionals, "total_additionals": round(total_additionals, 2),
"grand_total": round( "grand_total": round(
total_price_discounted + total_vat_amount + total_additionals, 2 total_price_discounted + total_vat_amount + total_additionals, 2
), ),
@ -1116,9 +1117,167 @@ class CarFinanceCalculator:
"total_discount": totals["total_discount"], "total_discount": totals["total_discount"],
"total_additionals": totals["total_additionals"], "total_additionals": totals["total_additionals"],
"grand_total": totals["grand_total"], "grand_total": totals["grand_total"],
"additionals": self.additional_services, "additionals": self._get_additional_services(),
"vat": self.vat_rate, "vat": self.vat_rate,
} }
# class CarFinanceCalculator:
# """
# Class responsible for calculating car financing details.
# This class provides methods and attributes required for calculating various
# aspects related to car financing, such as VAT calculation, pricing, discounts,
# and additional services. It processes data about cars, computes totals (e.g.,
# price, VAT, discounts), and aggregates the financial data for reporting or
# further processing.
# :ivar model: The data model passed to the calculator for retrieving transaction data.
# :type model: Any
# :ivar vat_rate: The current active VAT rate retrieved from the database.
# :type vat_rate: Decimal
# :ivar item_transactions: A collection of item transactions retrieved from the model.
# :type item_transactions: list
# :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status).
# :type additional_services: list
# """
# VAT_OBJ_NAME = "vat_rate"
# CAR_FINANCE_KEY = "car_finance"
# CAR_INFO_KEY = "car_info"
# ADDITIONAL_SERVICES_KEY = "additional_services"
# def __init__(self, model):
# if isinstance(model, InvoiceModel):
# self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity)
# self.extra_info = models.ExtraInfo.objects.get(
# dealer=self.dealer,
# content_type=ContentType.objects.get_for_model(model.ce_model),
# object_id=model.ce_model.pk,
# )
# elif isinstance(model, EstimateModel):
# self.dealer = models.Dealer.objects.get(entity=model.entity)
# self.extra_info = models.ExtraInfo.objects.get(
# dealer=self.dealer,
# content_type=ContentType.objects.get_for_model(model),
# object_id=model.pk,
# )
# self.model = model
# self.vat_rate = self._get_vat_rate()
# self.item_transactions = self._get_item_transactions()
# self.additional_services = self._get_additional_services()
# def _get_vat_rate(self):
# vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
# if not vat:
# raise ObjectDoesNotExist("No active VAT rate found")
# return vat.rate
# def _get_item_transactions(self):
# return self.model.get_itemtxs_data()[0].all()
# @staticmethod
# def _get_quantity(item):
# return item.ce_quantity or item.quantity
# def _get_nested_value(self, item, *keys):
# current = item.item_model.additional_info
# for key in keys:
# current = current.get(key, {})
# return current
# def _get_car_data(self, item):
# quantity = self._get_quantity(item)
# car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY)
# car_info = self._get_nested_value(item, self.CAR_INFO_KEY)
# unit_price = Decimal(car_finance.get("marked_price", 0))
# return {
# "item_number": item.item_model.item_number,
# "vin": car_info.get("vin"),
# "make": car_info.get("make"),
# "model": car_info.get("model"),
# "year": car_info.get("year"),
# "logo": getattr(item.item_model.car.id_car_make, "logo", ""),
# "trim": car_info.get("trim"),
# "mileage": car_info.get("mileage"),
# "cost_price": car_finance.get("cost_price"),
# "selling_price": car_finance.get("selling_price"),
# "marked_price": car_finance.get("marked_price"),
# "discount": car_finance.get("discount_amount"),
# "quantity": quantity,
# "unit_price": unit_price,
# "total": unit_price * Decimal(quantity),
# "total_vat": car_finance.get("total_vat"),
# "additional_services": self._get_nested_value(
# item, self.ADDITIONAL_SERVICES_KEY
# ),
# }
# def _get_additional_services(self):
# return [
# {
# "name": service.get("name"),
# "price": service.get("price"),
# "taxable": service.get("taxable"),
# "price_": service.get("price_"),
# }
# for item in self.item_transactions
# for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY)
# or []
# ]
# def calculate_totals(self):
# total_price = sum(
# Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "marked_price"))
# * int(self._get_quantity(item))
# for item in self.item_transactions
# )
# total_additionals = sum(
# Decimal(x.get("price_")) for x in self._get_additional_services()
# )
# total_discount = self.extra_info.data.get("discount", 0)
# # total_discount = sum(
# # Decimal(
# # self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount")
# # )
# # for item in self.item_transactions
# # )
# total_price_discounted = total_price
# if total_discount:
# total_price_discounted = total_price - Decimal(total_discount)
# total_vat_amount = total_price_discounted * self.vat_rate
# return {
# "total_price_before_discount": round(
# total_price, 2
# ), # total_price_before_discount,
# "total_price": round(total_price_discounted, 2), # total_price_discounted,
# "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount,
# "total_discount": round(Decimal(total_discount)),
# "total_additionals": round(total_additionals, 2), # total_additionals,
# "grand_total": round(
# total_price_discounted + total_vat_amount + total_additionals, 2
# ),
# }
# def get_finance_data(self):
# totals = self.calculate_totals()
# return {
# "cars": [self._get_car_data(item) for item in self.item_transactions],
# "quantity": sum(
# self._get_quantity(item) for item in self.item_transactions
# ),
# "total_price": totals["total_price"],
# "total_price_before_discount": totals["total_price_before_discount"],
# "total_vat": totals["total_vat_amount"] + totals["total_price"],
# "total_vat_amount": totals["total_vat_amount"],
# "total_discount": totals["total_discount"],
# "total_additionals": totals["total_additionals"],
# "grand_total": totals["grand_total"],
# "additionals": self.additional_services,
# "vat": self.vat_rate,
# }
def get_item_transactions(txs): def get_item_transactions(txs):
@ -1175,134 +1334,224 @@ def get_local_name(self):
return getattr(self, "name", None) return getattr(self, "name", None)
def handle_account_process(invoice, amount, finance_data):
@transaction.atomic
def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
""" """
Processes accounting transactions based on an invoice, financial data, Records the customer payment (`make_payment`) and posts the full
and related entity accounts configuration. This function handles the accounting (sales + VAT + COGS + Inventory).
creation of accounts if they do not already exist, and processes journal
entries and transactions.
:param invoice: The invoice object to process transactions for.
:type invoice: InvoiceModel
:param amount: Total monetary value for the transaction.
:type amount: Decimal
:param finance_data: Dictionary containing financial details such as
'grand_total', 'total_vat_amount', and other related data.
:type finance_data: dict
:return: None
""" """
for i in invoice.get_itemtxs_data()[0]: invoice.make_payment(amount)
# car = models.Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) invoice.save()
car = i.item_model.car
entity = invoice.ledger.entity
coa = entity.get_default_coa()
cash_account = ( _post_sale_and_cogs(invoice, dealer)
entity.get_all_accounts()
.filter(role_default=True, role=roles.ASSET_CA_CASH)
.first()
)
inventory_account = car.get_inventory_account()
revenue_account = car.get_revenue_account()
cogs_account = car.get_cogs_account()
# make_account = entity.get_all_accounts().filter(name=car.id_car_make.name,role=roles.COGS).first() def _post_sale_and_cogs(invoice, dealer):
# if not make_account: """
# last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first() For every car line on the invoice:
# if len(last_account.code) == 4: 1) Cash / A-R / VAT / Revenue journal
# code = f"{int(last_account.code)}{1:03d}" 2) COGS / Inventory journal
# elif len(last_account.code) > 4: """
# code = f"{int(last_account.code)+1}" entity = invoice.ledger.entity
calc = CarFinanceCalculator(invoice)
data = calc.get_finance_data()
# make_account = entity.create_account( cash_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
# name=car.id_car_make.name, ar_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
# code=code, vat_acc = entity.get_all_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
# role=roles.COGS, car_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
# coa_model=coa, add_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
# balance_type="debit", cogs_acc = entity.get_all_accounts().filter(role_default=True, role=roles.COGS).first()
# active=True inv_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
# )
# # get or create additional services account for car_data in data['cars']:
# additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first() car = invoice.get_itemtxs_data()[0].filter(
# if not additional_services_account: item_model__car__vin=car_data['vin']
# last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first() ).first().item_model.car
# if len(last_account.code) == 4: qty = Decimal(car_data['quantity'])
# code = f"{int(last_account.code)}{1:03d}"
# elif len(last_account.code) > 4:
# code = f"{int(last_account.code)+1}"
# additional_services_account = entity.create_account( net_car_price = Decimal(car_data['total'])
# name="Additional Services", net_add_price = Decimal(data['total_additionals'])
# code=code, vat_amount = Decimal(data['total_vat_amount']) * qty
# role=roles.COGS, grand_total = net_car_price + net_add_price + vat_amount
# coa_model=coa, cost_total = Decimal(car_data['cost_price']) * qty
# balance_type="debit",
# active=True
# )
# inventory_account = entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first() # ------------------------------------------------------------------
# 2A. Journal: Cash / A-R / VAT / Sales
# ------------------------------------------------------------------
# vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True) je_sale = JournalEntryModel.objects.create(
journal = JournalEntryModel.objects.create(
posted=False,
description=f"Payment for Invoice {invoice.invoice_number}",
ledger=invoice.ledger, ledger=invoice.ledger,
description=f"Sale {car.vin}",
origin=f"Invoice {invoice.invoice_number}",
locked=False, locked=False,
origin=f"Sale of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", posted=False
)
# Dr Cash (what the customer paid)
TransactionModel.objects.create(
journal_entry=je_sale,
account=cash_acc,
amount=grand_total,
tx_type='debit'
) )
# # Cr A/R (clear the receivable)
# TransactionModel.objects.create(
# journal_entry=je_sale,
# account=ar_acc,
# amount=grand_total,
# tx_type='credit'
# )
# Cr VAT Payable
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=journal, journal_entry=je_sale,
account=cash_account, account=vat_acc,
amount=Decimal(finance_data.get("grand_total")), amount=vat_amount,
tx_type="debit", tx_type='credit'
description="",
) )
# Cr Sales Car
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=journal, journal_entry=je_sale,
account=revenue_account, account=car_rev,
amount=Decimal(finance_data.get("grand_total")), amount=net_car_price,
tx_type="credit", tx_type='credit'
description="",
) )
journal_cogs = JournalEntryModel.objects.create( if net_add_price > 0:
posted=False, # Cr Sales Additional Services
description=f"COGS of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", TransactionModel.objects.create(
ledger=invoice.ledger, journal_entry=je_sale,
locked=False, account=add_rev,
origin="Payment", amount=net_add_price,
) tx_type='credit'
TransactionModel.objects.create(
journal_entry=journal_cogs,
account=cogs_account,
amount=Decimal(car.finances.cost_price),
tx_type="debit",
description="",
)
TransactionModel.objects.create(
journal_entry=journal_cogs,
account=inventory_account,
amount=Decimal(car.finances.cost_price),
tx_type="credit",
description="",
)
try:
car.item_model.for_inventory = False
logger.debug(f"Set item_model.for_inventory to False for car {car.vin}.")
except Exception as e:
logger.error(
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
exc_info=True,
) )
# ------------------------------------------------------------------
# 2B. Journal: COGS / Inventory reduction
# ------------------------------------------------------------------
je_cogs = JournalEntryModel.objects.create(
ledger=invoice.ledger,
description=f"COGS {car.vin}",
origin=f"Invoice {invoice.invoice_number}",
locked=False,
posted=False
)
# Dr COGS
TransactionModel.objects.create(
journal_entry=je_cogs,
account=cogs_acc,
amount=cost_total,
tx_type='debit'
)
# Cr Inventory
TransactionModel.objects.create(
journal_entry=je_cogs,
account=inv_acc,
amount=cost_total,
tx_type='credit'
)
# ------------------------------------------------------------------
# 2C. Update car state flags inside the same transaction
# ------------------------------------------------------------------
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
# car.item_model.for_inventory = False
# car.item_model.save(update_fields=['for_inventory'])
car.finances.selling_price = grand_total
car.finances.is_sold = True car.finances.is_sold = True
car.finances.save() car.finances.save()
car.item_model.save() # def handle_account_process(invoice, amount, finance_data):
# """
# Processes accounting transactions based on an invoice, financial data,
# and related entity accounts configuration. This function handles the
# creation of accounts if they do not already exist, and processes journal
# entries and transactions.
# :param invoice: The invoice object to process transactions for.
# :type invoice: InvoiceModel
# :param amount: Total monetary value for the transaction.
# :type amount: Decimal
# :param finance_data: Dictionary containing financial details such as
# 'grand_total', 'total_vat_amount', and other related data.
# :type finance_data: dict
# :return: None
# """
# for i in invoice.get_itemtxs_data()[0]:
# # car = models.Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name)
# car = i.item_model.car
# entity = invoice.ledger.entity
# coa = entity.get_default_coa()
# cash_account = (
# entity.get_all_accounts()
# .filter(role_default=True, role=roles.ASSET_CA_CASH)
# .first()
# )
# inventory_account = car.get_inventory_account()
# revenue_account = car.get_revenue_account()
# cogs_account = car.get_cogs_account()
# journal = JournalEntryModel.objects.create(
# posted=False,
# description=f"Payment for Invoice {invoice.invoice_number}",
# ledger=invoice.ledger,
# locked=False,
# origin=f"Sale of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=cash_account,
# amount=Decimal(finance_data.get("grand_total")),
# tx_type="debit",
# description="",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=revenue_account,
# amount=Decimal(finance_data.get("grand_total")),
# tx_type="credit",
# description="",
# )
# journal_cogs = JournalEntryModel.objects.create(
# posted=False,
# description=f"COGS of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}",
# ledger=invoice.ledger,
# locked=False,
# origin="Payment",
# )
# TransactionModel.objects.create(
# journal_entry=journal_cogs,
# account=cogs_account,
# amount=Decimal(car.finances.cost_price),
# tx_type="debit",
# description="",
# )
# TransactionModel.objects.create(
# journal_entry=journal_cogs,
# account=inventory_account,
# amount=Decimal(car.finances.cost_price),
# tx_type="credit",
# description="",
# )
# try:
# car.item_model.for_inventory = False
# logger.debug(f"Set item_model.for_inventory to False for car {car.vin}.")
# except Exception as e:
# logger.error(
# f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
# exc_info=True,
# )
# car.finances.is_sold = True
# car.finances.save()
# car.item_model.save()
# TransactionModel.objects.create( # TransactionModel.objects.create(
# journal_entry=journal, # journal_entry=journal,

View File

@ -67,7 +67,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo from plans.models import Plan, Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo
from django.views.generic import ( from django.views.generic import (
View, View,
ListView, ListView,
@ -145,6 +145,7 @@ from .override import (
BillModelUpdateView as BillModelUpdateViewBase, BillModelUpdateView as BillModelUpdateViewBase,
BaseBillActionView as BaseBillActionViewBase, BaseBillActionView as BaseBillActionViewBase,
InventoryListView as InventoryListViewBase, InventoryListView as InventoryListViewBase,
InvoiceModelUpdateView as InvoiceModelUpdateViewBase,
) )
from django_ledger.models import ( from django_ledger.models import (
@ -180,8 +181,6 @@ from django_ledger.views.mixins import (
) )
# Other # Other
from plans.models import Plan
from . import models, forms, tables from . import models, forms, tables
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -1145,7 +1144,6 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date") cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
context["stats"] = { context["stats"] = {
"all": cars.count(), "all": cars.count(),
"available": cars.filter(status="available").count(), "available": cars.filter(status="available").count(),
@ -1231,7 +1229,7 @@ def inventory_stats_view(request, dealer_slug):
# Base queryset for cars belonging to the dealer # Base queryset for cars belonging to the dealer
cars = models.Car.objects.filter(dealer=request.dealer) cars = models.Car.objects.filter(dealer=request.dealer)
print(cars)
# Count for total, reserved, showroom, and unreserved cars # Count for total, reserved, showroom, and unreserved cars
total_cars = cars.count() total_cars = cars.count()
reserved_cars = models.CarReservation.objects.count() reserved_cars = models.CarReservation.objects.count()
@ -1517,13 +1515,13 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
context["car"] = self.car context["car"] = self.car
return context return context
def get_form(self, form_class=None): # def get_form(self, form_class=None):
form = super().get_form(form_class) # form = super().get_form(form_class)
dealer = get_user_type(self.request) # dealer = get_user_type(self.request)
form.fields[ # form.fields[
"additional_finances" # "additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer) # ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form # return form
class CarFinanceUpdateView( class CarFinanceUpdateView(
@ -1570,21 +1568,21 @@ class CarFinanceUpdateView(
kwargs["instance"] = self.get_object() kwargs["instance"] = self.get_object()
return kwargs return kwargs
def get_initial(self): # def get_initial(self):
initial = super().get_initial() # initial = super().get_initial()
instance = self.get_object() # instance = self.get_object()
dealer = get_user_type(self.request) # dealer = get_user_type(self.request)
selected_items = instance.additional_services.filter(dealer=dealer) # selected_items = instance.additional_services.filter(dealer=dealer)
initial["additional_finances"] = selected_items # initial["additional_finances"] = selected_items
return initial # return initial
def get_form(self, form_class=None): # def get_form(self, form_class=None):
form = super().get_form(form_class) # form = super().get_form(form_class)
dealer = get_user_type(self.request) # dealer = get_user_type(self.request)
form.fields[ # form.fields[
"additional_finances" # "additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer) # ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form # return form
class CarUpdateView( class CarUpdateView(
@ -2405,7 +2403,6 @@ class CustomerCreateView(
success_message = "Customer created successfully" success_message = "Customer created successfully"
def form_valid(self, form): def form_valid(self, form):
sleep(5)
if customer := models.Customer.objects.filter( if customer := models.Customer.objects.filter(
email=form.instance.email email=form.instance.email
).first(): ).first():
@ -4328,12 +4325,12 @@ def sales_list_view(request, dealer_slug):
qs = [] qs = []
try: try:
if any([request.is_dealer, request.is_manager, request.is_accountant]): if any([request.is_dealer, request.is_manager, request.is_accountant]):
qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True) qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer)
elif request.is_staff: elif request.is_staff:
qs = models.ExtraInfo.get_sale_orders(staff=staff) qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer)
except Exception as e: except Exception as e:
print(e) print(e)
print(qs[0])
# query = request.GET.get('q') # query = request.GET.get('q')
# # if query: # # if query:
# # qs = qs.filter( # # qs = qs.filter(
@ -4418,7 +4415,6 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
staff = getattr(self.request.user.staffmember, "staff", None)
if any( if any(
[ [
@ -4431,16 +4427,19 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
dealer=dealer, dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff), related_content_type=ContentType.objects.get_for_model(models.Staff),
) ).union(models.ExtraInfo.objects.filter(
print(qs) dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(User),
))
elif self.request.is_staff and self.request.is_sales: elif self.request.is_staff and self.request.is_sales:
qs = models.ExtraInfo.objects.filter( qs = models.ExtraInfo.objects.filter(
dealer=dealer, dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff), related_content_type=ContentType.objects.get_for_model(models.Staff),
related_object_id=staff.pk, related_object_id=self.request.staff.pk,
) )
context["staff_estimates"] = qs context["staff_estimates"] = qs
return context return context
@ -4579,6 +4578,7 @@ def create_estimate(request, dealer_slug, slug=None):
).all() ).all()
for i in car_instance[: int(quantities[0])]: for i in car_instance[: int(quantities[0])]:
print(i)
items_txs.append( items_txs.append(
{ {
"item_number": i.item_model.item_number, "item_number": i.item_model.item_number,
@ -4641,11 +4641,11 @@ def create_estimate(request, dealer_slug, slug=None):
opportunity.estimate = estimate opportunity.estimate = estimate
opportunity.save() opportunity.save()
if staff := getattr(request.user.staffmember, "staff", None): if request.is_staff:
models.ExtraInfo.objects.create( models.ExtraInfo.objects.create(
dealer=dealer, dealer=dealer,
content_object=estimate, content_object=estimate,
related_object=staff, related_object=request.staff,
created_by=request.user, created_by=request.user,
) )
else: else:
@ -4839,21 +4839,21 @@ def create_sale_order(request, dealer_slug, pk):
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
form = forms.SaleOrderForm() form = forms.SaleOrderForm()
customer = estimate.customer.customer_set.first() # customer = estimate.customer.customer_set.first()
form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) # form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
form.initial["estimate"] = estimate # form.initial["estimate"] = estimate
form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk) # form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
form.initial["customer"] = customer # form.initial["customer"] = customer
if hasattr(estimate, "opportunity"): # if hasattr(estimate, "opportunity"):
form.initial["opportunity"] = estimate.opportunity # form.initial["opportunity"] = estimate.opportunity
else: # else:
form.fields["opportunity"].widget = HiddenInput() # form.fields["opportunity"].widget = HiddenInput()
calculator = CarFinanceCalculator(estimate) calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data() finance_data = calculator.get_finance_data()
return render( return render(
request, request,
"sales/estimates/sale_order_form1.html", "sales/estimates/sale_order_form.html",
{"form": form, "estimate": estimate, "items": items, "data": finance_data}, {"form": form, "estimate": estimate, "items": items, "data": finance_data},
) )
@ -4872,6 +4872,7 @@ def update_estimate_discount(request, dealer_slug, pk):
extra_info.data.update({"discount": Decimal(discount_amount)}) extra_info.data.update({"discount": Decimal(discount_amount)})
extra_info.save() extra_info.save()
messages.success(request, "Discount updated successfully")
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
@ -4888,7 +4889,7 @@ def update_estimate_additionals(request, dealer_slug, pk):
form.cleaned_data["additional_finances"] form.cleaned_data["additional_finances"]
) )
car.finances.save() car.finances.save()
messages.success(request, "Additional Finances updated successfully")
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
@ -5147,9 +5148,9 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
self.request.is_accountant, self.request.is_accountant,
] ]
): ):
qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True) qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer)
elif self.request.is_staff: elif self.request.is_staff:
qs = models.ExtraInfo.get_invoices(staff=staff) qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer)
except Exception as e: except Exception as e:
print(e) print(e)
@ -5285,7 +5286,7 @@ class ApprovedInvoiceModelUpdateFormView(
def get_success_url(self): def get_success_url(self):
return reverse_lazy( return reverse_lazy(
"invoice_detail", "invoice_detail",
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk}, kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk},
) )
@ -5333,7 +5334,7 @@ class PaidInvoiceModelUpdateFormView(
def get_success_url(self): def get_success_url(self):
return reverse_lazy( return reverse_lazy(
"invoice_detail", "invoice_detail",
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk}, kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk},
) )
def form_valid(self, form): def form_valid(self, form):
@ -5341,7 +5342,7 @@ class PaidInvoiceModelUpdateFormView(
if invoice.get_amount_open() > 0: if invoice.get_amount_open() > 0:
messages.error(self.request, "Invoice is not fully paid") messages.error(self.request, "Invoice is not fully paid")
return redirect("invoice_detail", pk=invoice.pk) return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk)
else: else:
invoice.post_ledger() invoice.post_ledger()
invoice.save() invoice.save()
@ -5373,12 +5374,12 @@ def invoice_mark_as(request, dealer_slug, pk):
if mark and mark == "accept": if mark and mark == "accept":
if not invoice.can_approve(): if not invoice.can_approve():
messages.error(request, "invoice is not ready for approval") messages.error(request, "invoice is not ready for approval")
return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk)
invoice.mark_as_approved( invoice.mark_as_approved(
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
) )
invoice.save() invoice.save()
return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk)
@login_required @login_required
@ -5441,7 +5442,7 @@ def invoice_create(request, dealer_slug, pk):
estimate.save() estimate.save()
invoice.save() invoice.save()
messages.success(request, "Invoice created successfully") messages.success(request, "Invoice created successfully")
return redirect("invoice_detail", dealer_slug=dealer.slug, pk=invoice.pk) return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk)
else: else:
print(form.errors) print(form.errors)
form = forms.InvoiceModelCreateForm( form = forms.InvoiceModelCreateForm(
@ -5501,6 +5502,32 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
# payments # payments
class InvoiceModelUpdateView(InvoiceModelUpdateViewBase):
template_name = 'sales/invoices/invoice_update.html'
permission_required = ["django_ledger.change_invoicemodel"]
# def PaymentCreateView(request,dealer_slug,entity_slug,invoice_pk):
# from django_ledger.forms.invoice import AccruedAndApprovedInvoiceModelUpdateForm
# invoice = get_object_or_404(InvoiceModel,pk=invoice_pk)
# form = AccruedAndApprovedInvoiceModelUpdateForm(entity_slug=entity_slug,user_model=request.dealer.user)
# if request.method == "POST":
# if form.is_valid():
# invoice_model: InvoiceModel = form.save(commit=False)
# if invoice_model.can_migrate():
# invoice_model.migrate_state(
# user_model=request.dealer.user,
# entity_slug=entity_slug
# )
# invoice_model.save()
# messages.success(request, "Invoice updated successfully")
# return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=entity_slug, invoice_pk=invoice_model.pk)
# else:
# print(form.errors)
# context = { "invoice": invoice, "form": form }
# return render(request, "sales/payments/payment_form1.html", context)
@login_required @login_required
@permission_required("inventory.add_payment", raise_exception=True) @permission_required("inventory.add_payment", raise_exception=True)
@ -5529,15 +5556,15 @@ def PaymentCreateView(request, dealer_slug, pk):
""" """
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
invoice = InvoiceModel.objects.filter(pk=pk).first() invoice = InvoiceModel.objects.filter(pk=pk).first()
bill = BillModel.objects.filter(pk=pk).first() # bill = BillModel.objects.filter(pk=pk).first()
model = invoice if invoice else bill model = invoice
entity = dealer.entity entity = dealer.entity
form = forms.PaymentForm() form = forms.PaymentForm()
breakpoint()
if request.method == "POST": if request.method == "POST":
form = forms.PaymentForm(request.POST) form = forms.PaymentForm(request.POST)
# --- Define user and model context for logging here
user_id = request.user.id if request.user.is_authenticated else "Anonymous" user_id = request.user.id if request.user.is_authenticated else "Anonymous"
user_username = ( user_username = (
request.user.username if request.user.is_authenticated else "anonymous" request.user.username if request.user.is_authenticated else "anonymous"
@ -5546,19 +5573,19 @@ def PaymentCreateView(request, dealer_slug, pk):
if form.is_valid(): if form.is_valid():
amount = form.cleaned_data.get("amount") amount = form.cleaned_data.get("amount")
invoice = form.cleaned_data.get("invoice") invoice = form.cleaned_data.get("invoice")
bill = form.cleaned_data.get("bill") # bill = form.cleaned_data.get("bill")
payment_method = form.cleaned_data.get("payment_method") payment_method = form.cleaned_data.get("payment_method")
redirect_url = "invoice_detail" if invoice else "bill_detail" response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail"
model = invoice if invoice else bill # model = invoice if invoice else bill
if not model.is_approved(): if not model.is_approved():
model.mark_as_approved(user_model=entity.admin) model.mark_as_approved(user_model=entity.admin)
if model.amount_paid == model.amount_due: if model.amount_paid == model.amount_due:
messages.error(request, _("fully paid")) messages.error(request, _("fully paid"))
return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) return response
if model.amount_paid + amount > model.amount_due: if model.amount_paid + amount > model.amount_due:
messages.error(request, _("Amount exceeds due amount")) messages.error(request, _("Amount exceeds due amount"))
return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) return response
try: try:
if invoice: if invoice:
@ -5566,14 +5593,14 @@ def PaymentCreateView(request, dealer_slug, pk):
logger.info( logger.info(
f"User {user_username} (ID: {user_id}) successfully processed payment for Invoice ID: {invoice.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}." f"User {user_username} (ID: {user_id}) successfully processed payment for Invoice ID: {invoice.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
) )
elif bill: # elif bill:
set_bill_payment(dealer, entity, bill, amount, payment_method) # set_bill_payment(dealer, entity, bill, amount, payment_method)
logger.info( # logger.info(
f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}." # f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
) # )
messages.success(request, _("Payment created successfully")) messages.success(request, _("Payment created successfully"))
return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) return response
except Exception as e: except Exception as e:
logger.error( logger.error(
f"User {user_username} (ID: {user_id}) encountered error creating payment " f"User {user_username} (ID: {user_id}) encountered error creating payment "
@ -5594,7 +5621,7 @@ def PaymentCreateView(request, dealer_slug, pk):
form.initial["bill"] = model form.initial["bill"] = model
form.fields["invoice"].widget = HiddenInput() form.fields["invoice"].widget = HiddenInput()
return render( return render(
request, "sales/payments/payment_form.html", {"model": model, "form": form} request, "sales/payments/payment_form1.html", {"model": model, "form": form}
) )
@ -5725,7 +5752,7 @@ def payment_mark_as_paid(request, dealer_slug, pk):
exc_info=True, exc_info=True,
) )
messages.error(request, f"Error: {str(e)}") messages.error(request, f"Error: {str(e)}")
return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk)
# activity log # activity log
@ -6577,41 +6604,43 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None):
) )
messages.success(request, _("Email Draft successfully")) messages.success(request, _("Email Draft successfully"))
try: # try:
if getattr(lead, "opportunity", None): # if getattr(lead, "opportunity", None):
# Log success when opportunity exists and redirecting # # Log success when opportunity exists and redirecting
logger.info( # logger.info(
f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " # f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." # f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
) # )
response = HttpResponse( # # response = HttpResponse(
redirect( # # redirect(
"opportunity_detail", # # "opportunity_detail",
dealer_slug=dealer_slug, # # dealer_slug=dealer_slug,
slug=lead.opportunity.slug, # # slug=lead.opportunity.slug,
) # # )
) # # )
response["HX-Redirect"] = reverse( # # response["HX-Redirect"] = reverse(
"opportunity_detail", args=[lead.opportunity.slug] # # "opportunity_detail", args=[lead.opportunity.slug]
) # # )
else:
# Log success when no opportunity and redirecting to lead detail # else:
logger.info( # # Log success when no opportunity and redirecting to lead detail
f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " # logger.info(
f"Lead has no Opportunity, redirecting to lead detail." # f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
) # f"Lead has no Opportunity, redirecting to lead detail."
response = HttpResponse() # )
response["HX-Redirect"] = reverse( # # response = HttpResponse()
"lead_detail", dealer_slug=dealer_slug, slug=lead.slug # # response["HX-Redirect"] = reverse(
) # # "lead_detail", dealer_slug=dealer_slug, slug=lead.slug
return response # # )
except models.Lead.opportunity.RelatedObjectDoesNotExist: # return response
# --- Log when Lead.opportunity does not exist (Draft status) --- # except models.Lead.opportunity.RelatedObjectDoesNotExist:
logger.info( # # --- Log when Lead.opportunity does not exist (Draft status) ---
f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " # logger.info(
f"Lead's opportunity does not exist. Redirecting to lead list." # f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
) # f"Lead's opportunity does not exist. Redirecting to lead list."
return redirect("lead_list", dealer_slug=dealer.slug) # )
# return response
# return redirect("lead_list", dealer_slug=dealer.slug)
if request.method == "POST": if request.method == "POST":
email_pk = request.POST.get("email_pk") email_pk = request.POST.get("email_pk")
@ -6643,25 +6672,30 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None):
activity_type=models.ActionChoices.EMAIL, activity_type=models.ActionChoices.EMAIL,
) )
messages.success(request, _("Email sent successfully")) messages.success(request, _("Email sent successfully"))
try: response = HttpResponse()
if lead.opportunity: response["HX-Refresh"] = "true"
# Log success when opportunity exists and redirecting after sending email return response
logger.info( # try:
f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). " # if lead.opportunity:
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." # # Log success when opportunity exists and redirecting after sending email
) # logger.info(
return redirect( # f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
"opportunity_detail", # f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
dealer_slug=dealer_slug, # )
slug=lead.opportunity.slug, # return response
) # # return redirect(
except models.Lead.opportunity.RelatedObjectDoesNotExist: # # "opportunity_detail",
# --- Log when Lead.opportunity does not exist (POST request for sending) --- # # dealer_slug=dealer_slug,
logger.info( # # slug=lead.opportunity.slug,
f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). " # # )
f"Lead's opportunity does not exist. Redirecting to lead list." # except models.Lead.opportunity.RelatedObjectDoesNotExist:
) # # --- Log when Lead.opportunity does not exist (POST request for sending) ---
return redirect("lead_list", dealer_slug=dealer_slug) # logger.info(
# f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead's opportunity does not exist. Redirecting to lead list."
# )
# return response
# return redirect("lead_list", dealer_slug=dealer_slug)
msg = f""" msg = f"""
السلام عليكم السلام عليكم
Dear {lead.full_name}, Dear {lead.full_name},
@ -6745,14 +6779,18 @@ class OpportunityCreateView(
def get_form(self, form_class=None): def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
staff = getattr(self.request.user.staffmember, "staff", None)
form = super().get_form(form_class) form = super().get_form(form_class)
form.fields["car"].queryset = models.Car.objects.filter( form.fields["car"].queryset = models.Car.objects.filter(
dealer=dealer, status="available", finances__marked_price__gt=0 dealer=dealer, status="available", finances__marked_price__gt=0
) )
form.fields["lead"].queryset = models.Lead.objects.filter( if self.request.is_dealer:
dealer=dealer, staff=staff form.fields["lead"].queryset = models.Lead.objects.filter(
) dealer=dealer
)
elif self.request.is_staff:
form.fields["lead"].queryset = models.Lead.objects.filter(
dealer=dealer, staff=self.request.staff
)
return form return form
def get_success_url(self): def get_success_url(self):
@ -9304,7 +9342,7 @@ def sse_stream(request):
) )
last_id = notification.id last_id = notification.id
sleep(2) sleep(3)
response = StreamingHttpResponse(event_stream(), content_type="text/event-stream") response = StreamingHttpResponse(event_stream(), content_type="text/event-stream")
response["Cache-Control"] = "no-cache" response["Cache-Control"] = "no-cache"
@ -10258,6 +10296,12 @@ def upload_cars(request, dealer_slug, pk=None):
csv_data = io.StringIO(file_content) csv_data = io.StringIO(file_content)
reader = csv.DictReader(csv_data) reader = csv.DictReader(csv_data)
data = [x for x in reader] data = [x for x in reader]
if len(data) < item.quantity:
messages.error(
request,
f"CSV file has {len(data)} rows, but the quantity of the item is {item.quantity}.",
)
return response
for row in data: for row in data:
# Log VIN decoding and initial validation for each row # Log VIN decoding and initial validation for each row
logger.debug( logger.debug(

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
static/images/user-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
static/user-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,51 @@
// static/js/formSubmitHandler.js
document.addEventListener('DOMContentLoaded', function() {
// Initialize all forms with submit buttons
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
// Store original button HTML
if (!submitBtn.dataset.originalHtml) {
submitBtn.dataset.originalHtml = submitBtn.innerHTML;
}
form.addEventListener('submit', function(e) {
// Only proceed if form is valid
if (form.checkValidity()) {
disableSubmitButton(submitBtn);
}
});
}
});
});
/**
* Disable and show loading state on submit button
* @param {HTMLElement} button - The submit button element
*/
function disableSubmitButton(button) {
button.disabled = true;
button.innerHTML = `
<span class="submit-spinner">
<i class="fas fa-spinner fa-spin me-1"></i>
</span>
<span class="submit-text">${button.dataset.savingText || 'Processing...'}</span>
`;
button.classList.add('submitting');
}
/**
* Reset submit button to original state
* @param {HTMLElement} button - The submit button element
*/
function resetSubmitButton(button) {
if (button.dataset.originalHtml) {
button.innerHTML = button.dataset.originalHtml;
}
button.disabled = false;
button.classList.remove('submitting');
}

BIN
staticfiles/user-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

View File

@ -54,14 +54,8 @@
<link href="{% static 'css/custom.css' %}" rel="stylesheet"> <link href="{% static 'css/custom.css' %}" rel="stylesheet">
{% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %} {% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %} {% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}" <link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
type="text/css" <link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
rel="stylesheet"
id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="user-style-rtl">
{% else %} {% else %}
<link href="{% static 'css/theme.min.css' %}" <link href="{% static 'css/theme.min.css' %}"
type="text/css" type="text/css"

View File

@ -14,8 +14,7 @@
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %} {% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" <form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
method="post">
{% csrf_token %} {% csrf_token %}
<div class="mb-3">{{ form|crispy }}</div> <div class="mb-3">{{ form|crispy }}</div>
<button type="submit" class="btn btn-phoenix-primary mb-2 me-2"> <button type="submit" class="btn btn-phoenix-primary mb-2 me-2">

View File

@ -2,8 +2,14 @@
{% load static %} {% load static %}
{% load django_ledger %} {% load django_ledger %}
{% load widget_tweaks %} {% load widget_tweaks %}
<form action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post"> {% if bill.get_itemtxs_data.1.total_amount__sum > 0 %}
<form id="bill-update-form" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% else %}
<form id="bill-update-form" hx-trigger="load delay:300ms" hx-swap="outerHTML" hx-target="#bill-update-form" hx-select="#bill-update-form" hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% endif %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Page Header --> <!-- Page Header -->
<div class="row mb-4"> <div class="row mb-4">

View File

@ -0,0 +1,16 @@
{% load i18n crispy_forms_tags %}
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div id="emailModalBody" class="modal-body">
<h1>hi</h1>
</div>
</div>
</div>
</div>

View File

@ -64,14 +64,8 @@
<div class="card-body"> <div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start"> <div class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto flex-1"> <div class="col-6 col-sm-auto flex-1">
<h3 class="fw-bolder mb-2">{{ lead.first_name }} {{ lead.last_name }}</h3> <h3 class="fw-bolder mb-2">{{ lead.first_name|capfirst }} {{ lead.last_name|capfirst }}</h3>
{% if lead.staff %} <p>{{ lead.email|capfirst }}</p>
<p class="fs-8 mb-0 white-space-nowrap fw-bold">
{{ _("Assigned to") }}: <span class="fw-light">{{ lead.staff }}</span>
</p>
{% else %}
<p class="mb-0 fw-bold">{{ _("Not Assigned") }}</p>
{% endif %}
</div> </div>
<div class="col-6 col-sm-auto flex-1"> <div class="col-6 col-sm-auto flex-1">
<h5 class="text-body-highlight mb-0 text-end"> <h5 class="text-body-highlight mb-0 text-end">
@ -107,6 +101,34 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card mb-2">
<div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
<div class="d-flex align-items-center">
<div class="avatar avatar-tiny me-2">
{% if lead.staff.logo %}
<img class="avatar-img rounded-circle"
src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo">
{% endif %}
</div>
<small>
{% if lead.staff == request.staff %}
{{ _("Me") }}
{% elif LANGUAGE_CODE == "en" %}
{{ lead.staff.name|capfirst }}
{% else %}
{{ lead.staff.arabic_name }}
{% endif %}
</small>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start"> <div class="row align-items-center g-3 text-center text-xxl-start">
@ -492,12 +514,23 @@
<div class="mb-1 d-flex justify-content-between align-items-center"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3> <h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3>
{% if perms.inventory.change_lead %} {% if perms.inventory.change_lead %}
<a href="{% url 'send_lead_email' request.dealer.slug lead.slug %}"> {% comment %} <a href="{% url 'send_lead_email' request.dealer.slug lead.slug %}">
<button type="button" class="btn btn-sm btn-phoenix-primary"> <button type="button" class="btn btn-sm btn-phoenix-primary">
<span class="fas fa-plus me-1"></span> <span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %} {% trans 'Send Email' %}
</button> </button>
</a> </a> {% endcomment %}
<button class="btn btn-phoenix-primary btn-sm"
type="button"
data-bs-toggle="modal"
data-bs-target="#emailModal"
hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %} {% endif %}
</div> </div>
<div> <div>
@ -791,21 +824,7 @@
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
<!-- add update Modal --> <!-- add update Modal -->
{% comment %} <div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true"> {% include "components/email_modal.html" %}
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="noteModalLabel">{% trans 'Notes' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body">
<!-- Content will be loaded here via AJAX -->
</div>
</div>
</div>
</div> {% endcomment %}
<!-- task Modal --> <!-- task Modal -->
{% include "components/task_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
<!-- note Modal --> <!-- note Modal -->

View File

@ -42,7 +42,7 @@
</h3> </h3>
</div> </div>
<div class="card-body bg-light-subtle"> <div class="card-body bg-light-subtle">
<form class="form" method="post"> <form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<hr class="my-2"> <hr class="my-2">

View File

@ -8,7 +8,8 @@
<div class="card email-content"> <div class="card email-content">
<h5 class="card-header">Send Mail</h5> <h5 class="card-header">Send Mail</h5>
<div class="card-body"> <div class="card-body">
<form class="d-flex flex-column h-100" <form class="email-form d-flex flex-column h-100"
hx-boost="true"
action="{% url 'send_lead_email' request.dealer.slug lead.slug %}" action="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
@ -47,7 +48,7 @@
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-phoenix-danger">Discard</a> {% comment %} <a href="{{ request.META.HTTP_REFERER }}" class="btn btn-phoenix-danger">Discard</a> {% endcomment %}
<a hx-boost="true" <a hx-boost="true"
hx-push-url='false' hx-push-url='false'
hx-include="#message,#subject,#to" hx-include="#message,#subject,#to"

View File

@ -180,7 +180,7 @@
</div> </div>
{% if opportunity.estimate.invoice %} {% if opportunity.estimate.invoice %}
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'invoice_detail' opportunity.estimate.invoice.pk %}">{{ _("View Invoice") }}</a> href="{% url 'invoice_detail' request.dealer.slug request.entity.slug opportunity.estimate.invoice.pk %}">{{ _("View Invoice") }}</a>
{% else %} {% else %}
<p>{{ _("No Invoice") }}</p> <p>{{ _("No Invoice") }}</p>
{% endif %} {% endif %}
@ -819,12 +819,25 @@
<h2 class="mb-4">Emails</h2> <h2 class="mb-4">Emails</h2>
{% if perms.inventory.change_opportunity %} {% if perms.inventory.change_opportunity %}
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<a href="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"> {% comment %} <a href="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}">
<button type="button" class="btn btn-sm btn-phoenix-primary"> <button type="button" class="btn btn-sm btn-phoenix-primary">
<span class="fas fa-plus me-1"></span> <span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %} {% trans 'Send Email' %}
</button> </button>
</a> </a> {% endcomment %}
{% if opportunity.lead %}
<button class="btn btn-phoenix-primary btn-sm"
type="button"
data-bs-toggle="modal"
data-bs-target="#emailModal"
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %}
</div> </div>
{% endif %} {% endif %}
<div> <div>
@ -843,15 +856,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="search-box w-100 mb-3">
<form class="position-relative">
<input class="form-control search-input search"
type="search"
placeholder="Search..."
aria-label="Search" />
<span class="fas fa-search search-box-icon"></span>
</form>
</div>
<div class="tab-content" id="profileTabContent"> <div class="tab-content" id="profileTabContent">
<div class="tab-pane fade show active" <div class="tab-pane fade show active"
id="tab-mail" id="tab-mail"
@ -864,13 +869,7 @@
<table class="table fs-9 mb-0"> <table class="table fs-9 mb-0">
<thead> <thead>
<tr> <tr>
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
<div class="form-check mb-0 fs-8">
<input class="form-check-input"
type="checkbox"
data-bulk-select='{"body":"all-email-table-body"}' />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" <th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col" scope="col"
data-sort="subject" data-sort="subject"
@ -884,11 +883,7 @@
<th class="sort align-middle text-start text-uppercase" <th class="sort align-middle text-start text-uppercase"
scope="col" scope="col"
data-sort="date" data-sort="date"
style="min-width:165px">Date</th> style="min-width:15px">Date</th>
<th class="sort align-middle pe-0 text-uppercase"
scope="col"
style="width:15%;
min-width:100px">Action</th>
<th class="sort align-middle text-end text-uppercase" <th class="sort align-middle text-end text-uppercase"
scope="col" scope="col"
data-sort="status" data-sort="status"
@ -899,22 +894,14 @@
<tbody class="list" id="all-email-table-body"> <tbody class="list" id="all-email-table-body">
{% for email in opportunity.lead.get_emails %} {% for email in opportunity.lead.get_emails %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">
<input class="form-check-input"
type="checkbox"
data-bulk-select-row='{"mail":{"subject":"Quary about purchased soccer socks","email":"jackson@mail.com"},"active":true,"sent":"Jackson Pollock","date":"Dec 29, 2021 10:23 am","source":"Call","type_status":{"label":"sent","type":"badge-phoenix-success"}}' />
</div>
</td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"> <td class="subject order align-middle white-space-nowrap py-2 ps-0">
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a> <a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
<div class="fs-10 d-block">{{ email.to_email }}</div> <div class="fs-10 d-block">{{ email.to_email }}</div>
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</td> <td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td> <td class="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td>
<td class="align-middle white-space-nowrap ps-3">
<a class="text-body" href="#!"><span class="fa-solid fa-phone text-primary me-2"></span>Call</a>
</td>
<td class="status align-middle fw-semibold text-end py-2"> <td class="status align-middle fw-semibold text-end py-2">
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span> <span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
</td> </td>
@ -1103,6 +1090,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- email Modal -->
{% include "components/email_modal.html" %}
<!-- task Modal --> <!-- task Modal -->
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- note Modal --> <!-- note Modal -->

View File

@ -123,13 +123,13 @@
<button type="reset" class="btn btn-phoenix-danger px-4"> <button type="reset" class="btn btn-phoenix-danger px-4">
<span class="fas fa-redo me-1"></span>{% trans "Reset" %} <span class="fas fa-redo me-1"></span>{% trans "Reset" %}
</button> </button>
{% if form.instance.pk %} <button type="submit" class="btn btn-phoenix-primary px-6">
<button type="submit" class="btn btn-phoenix-primary px-6"> {% if form.instance.pk %}
<span class="fas fa-save me-1"></span>{% trans "Update" %} <span class="fas fa-save me-1"></span>{% trans "Update" %}
{% else %} {% else %}
<span class="fas fa-plus me-1"></span>{% trans "Create" %} <span class="fas fa-plus me-1"></span>{% trans "Create" %}
{% endif %} {% endif %}
</button> </button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -262,6 +262,10 @@
<td>{{ car.finances.cost_price|floatformat:2 }}</td> <td>{{ car.finances.cost_price|floatformat:2 }}</td>
{% endif %} {% endif %}
</tr> </tr>
<tr>
<th>{% trans "Marked Price"|capfirst %}</th>
<td>{{ car.finances.marked_price|floatformat:2 }}</td>
</tr>
<tr> <tr>
<th>{% trans "Selling Price"|capfirst %}</th> <th>{% trans "Selling Price"|capfirst %}</th>
<td>{{ car.finances.selling_price|floatformat:2 }}</td> <td>{{ car.finances.selling_price|floatformat:2 }}</td>

View File

@ -30,7 +30,7 @@
<td class="align-middle product white-space-nowrap px-1"> <td class="align-middle product white-space-nowrap px-1">
{% if ledger.invoicemodel %} {% if ledger.invoicemodel %}
{% if perms.django_ledger.view_invoicemodel %} {% if perms.django_ledger.view_invoicemodel %}
<a href="{% url 'invoice_detail' request.dealer.slug ledger.invoicemodel.pk %}">{{ ledger.get_wrapped_model_instance }}</a> <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug ledger.invoicemodel.pk %}">{{ ledger.get_wrapped_model_instance }}</a>
{% endif %} {% endif %}
{% elif ledger.billmodel %} {% elif ledger.billmodel %}
{% if perms.django_ledger.view_billmodel %} {% if perms.django_ledger.view_billmodel %}

View File

@ -77,6 +77,16 @@
</li> </li>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
} });
let lastNotificationId = {{ notifications_.last.id|default:0 }}; let lastNotificationId = {{ notifications_.last.id|default:0 }};
let seenNotificationIds = new Set(); let seenNotificationIds = new Set();
@ -115,7 +125,10 @@
updateCounter(unreadCount); updateCounter(unreadCount);
connectSSE();
setTimeout(() => {
connectSSE();
}, 5000);
} }
}) })
.catch(error => { .catch(error => {
@ -129,7 +142,7 @@
eventSource.close(); eventSource.close();
} }
eventSource = new EventSource("{% url 'sse_stream' %}?last_id=" + lastNotificationId); eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId);
eventSource.addEventListener('notification', function(e) { eventSource.addEventListener('notification', function(e) {
try { try {
@ -152,6 +165,11 @@
console.log("Audio play failed - may need user interaction first:", e); console.log("Audio play failed - may need user interaction first:", e);
}); });
Toast.fire({
icon: 'info',
html:`${data.message}`
});
} catch (error) { } catch (error) {
console.error('Error processing notification:', error); console.error('Error processing notification:', error);
} }
@ -159,7 +177,8 @@
eventSource.addEventListener('error', function(e) { eventSource.addEventListener('error', function(e) {
console.error('SSE connection error:', e); console.error('SSE connection error:', e);
setTimeout(connectSSE, 5000); eventSource.close();
setTimeout(connectSSE, 8000);
}); });
} }

View File

@ -157,7 +157,7 @@
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a> class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a>
{% endif %} {% endif %}
{% if perms.django_ledger.view_invoicemodel %} {% if perms.django_ledger.view_invoicemodel %}
<a href="{% url 'invoice_detail' request.dealer.slug estimate.invoicemodel_set.first.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug estimate.invoicemodel_set.first.pk %}"
class="btn btn-phoenix-primary btn-sm" class="btn btn-phoenix-primary btn-sm"
type="button"><i class="fa-solid fa-receipt"></i> type="button"><i class="fa-solid fa-receipt"></i>
{{ _("View Invoice") }}</a> {{ _("View Invoice") }}</a>
@ -286,7 +286,7 @@
type="button" type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#additionalModal"> data-bs-target="#additionalModal">
<span class="fas fa-plus me-1"></span>{{ _("Add") }} <span class="fas fa-plus me-1"></span>{{ _("") }}
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -1,248 +1,35 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% load i18n static %}
{% load crispy_forms_filters %} {% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %} {% block title %}
{% trans 'Sale Order' %} <h1>{% trans 'Sale Order' %}</h1>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<link rel="stylesheet" href="{% static 'flags/sprite.css' %}" /> <div class="row justify-content-center mt-5 mb-3">
<div class="row"> <div class="col-lg-8 col-md-10">
<div class="row mb-3"> <div class="card shadow-sm border-0 rounded-3">
<div class="col-sm-6 col-md-8"> <div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
<div class="d-sm-flex justify-content-between"> <h3 class="mb-0 fs-4 text-center text-white">
<h3 class="mb-3"> {% trans 'Sale Order' %}
{% if customer.created %}
{{ _("Edit Sale Order") }}
{% else %}
{{ _("Add Sale Order") }}
{% endif %}
</h3> </h3>
</div> </div>
</div> <div class="card-body bg-light-subtle">
</div> <form method="post" action="">
<div class="row mb-3"> {% csrf_token %}
<div class="col-xl-12 col-xxl-12"> {{ form|crispy }}
<div class="px-xl-12"> <hr class="my-2">
<div class="row mx-0 mx-sm-3 mx-lg-0 px-lg-0"> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<div class="col-sm-12 col-xxl-6 py-3"> <button class="btn btn-lg btn-phoenix-success md-me-2" type="submit">
<table class="w-100 table-stats "> <i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
<tr> </button>
<th></th> <a href="{{ request.META.HTTP_REFERER }}"
<th></th> class="btn btn-lg btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<th></th>
</tr>
<tr>
<td class="py-2">
<div class="d-inline-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="user"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Customer Name") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ estimate.customer.customer_name }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="mail"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Email") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.email }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="map-pin"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Address") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.address_1 }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="trending-down"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Total Discount") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">
{{ data.total_discount }} <span class="icon-saudi_riyal"></span>
</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="briefcase"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Total Amount") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">
{{ data.grand_total }} <span class="icon-saudi_riyal"></span>
</p>
</td>
</tr>
</table>
</div> </div>
</div> </form>
<div class="row">
<div class="border-top border-bottom border-translucent mt-10"
id="leadDetailsTable">
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col"
data-sort="name"
style="width:20%;
min-width:100px">{{ _("VIN") }}</th>
<th class="sort align-middle pe-6 text-uppercase"
scope="col"
data-sort="description"
style="width:20%;
max-width:60px">{{ _("Make") }}</th>
<th class="sort align-middle text-start text-uppercase"
scope="col"
data-sort="create_date"
style="width:20%;
min-width:115px">{{ _("Model") }}</th>
<th class="sort align-middle text-start text-uppercase"
scope="col"
data-sort="create_by"
style="width:20%;
min-width:150px">{{ _("Year") }}</th>
<th class="sort align-middle text-start text-uppercase"
scope="col"
data-sort="create_by"
style="width:20%;
min-width:150px">{{ _("Unit Price") }}</th>
<th class="align-middle pe-0 text-end" scope="col" style="width:15%;"></th>
</tr>
</thead>
<tbody class="list" id="lead-details-table-body">
{% for car in data.cars %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" />
</div>
</td>
<td class="name align-middle white-space-nowrap py-2 ps-0">
<a class="d-flex align-items-center text-body-highlight" href="#!">
{% comment %} <div class="avatar avatar-m me-3 status-online">
<img class="rounded-circle" src="" alt="" />
</div> {% endcomment %}
<h6 class="mb-0 text-body-highlight fw-bold">{{ car.vin }}</h6>
</a>
</td>
<td class="description align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2 pe-6">
{{ car.make }}
</td>
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.model }}</td>
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.year }}</td>
<td class="last_activity align-middle text-center py-2">
<div class="d-flex align-items-center flex-1">
<span class="fw-bold fs-9 text-body">{{ car.total }} <span class="icon-saudi_riyal"></span></span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row mx-0">
<form method="post" class="form row g-3 needs-validation" novalidate>
{% csrf_token %}
<div class="col-md-6">
<label for="id_estimate" class="form-label">{% trans "Quotation" %}</label>
<input type="text"
class="form-control form-control-sm"
id="id_estimate"
name="estimate"
value="{{ form.estimate.value|default_if_none:'' }}"
readonly>
<div class="invalid-feedback">{% trans "Please provide an estimate." %}</div>
</div>
<div class="col-md-6">
<label for="id_payment_method" class="form-label">{% trans "Payment Method" %}</label>
<select class="form-select form-select-sm"
id="id_payment_method"
name="payment_method"
required>
{% for value, label in form.payment_method.field.choices %}
<option value="{{ value }}"
{% if form.payment_method.value == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">{% trans "Please select a payment method." %}</div>
</div>
<div class="col-12">
<label for="id_comments" class="form-label">{% trans "Comments" %}</label>
<textarea class="form-control" id="id_comments" name="comments" rows="3">{{ form.comments.value|default_if_none:'' }}</textarea>
</div>
<div class="col-12">
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Save' %}</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} <!---->
{% endblock content %}

View File

@ -121,7 +121,10 @@
{% endif %} {% endif %}
{% if invoice.invoice_status == 'approved' %} {% if invoice.invoice_status == 'approved' %}
{% if perms.inventory.add_payment %} {% if perms.inventory.add_payment %}
{% comment %} <a href="{% url 'payment_create' request.dealer.slug invoice.pk %}"
class="btn btn-phoenix-success"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-money-bill"></i> {% trans 'Record Payment' %}</span></a> {% endcomment %}
<a href="{% url 'payment_create' request.dealer.slug invoice.pk %}" <a href="{% url 'payment_create' request.dealer.slug invoice.pk %}"
{% comment %} <a href="{% url 'invoice_update' request.dealer.slug request.entity.slug invoice.pk %}" {% endcomment %}
class="btn btn-phoenix-success"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-money-bill"></i> {% trans 'Record Payment' %}</span></a> class="btn btn-phoenix-success"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-money-bill"></i> {% trans 'Record Payment' %}</span></a>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -64,7 +64,7 @@
</td> </td>
<td class="align-middle product white-space-nowrap">{{ invoice.created }}</td> <td class="align-middle product white-space-nowrap">{{ invoice.created }}</td>
<td class="align-middle product white-space-nowrap"> <td class="align-middle product white-space-nowrap">
<a href="{% url 'invoice_detail' request.dealer.slug invoice.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
class="btn btn-sm btn-phoenix-success"> class="btn btn-sm btn-phoenix-success">
<i class="fa-regular fa-eye me-1"></i> <i class="fa-regular fa-eye me-1"></i>
{% trans "View" %} {% trans "View" %}

View File

@ -0,0 +1,29 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load crispy_forms_tags %}
{% load django_ledger %}
{% load custom_filters %}
{% block content %}
<div class="row justify-content-center mt-5 mb-3">
<div class="col-lg-8 col-md-10 ">
<div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
<h3 class="mb-0 fs-4 text-center text-white">{{ _("Add Payment") }}</h3>
</div>
<div class="card-body bg-light-subtle">
<form action="{% url 'invoice_update' request.dealer.slug request.entity.slug invoice.pk %}"
method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit"
class="btn btn-primary w-100 my-1">{% trans 'Save Invoice' %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -27,7 +27,7 @@
<td class="align-middle product white-space-nowrap">{{ journal. }}</td> <td class="align-middle product white-space-nowrap">{{ journal. }}</td>
<td class="align-middle product white-space-nowrap">{{ journal. }}</td> <td class="align-middle product white-space-nowrap">{{ journal. }}</td>
<td class="text-center"> <td class="text-center">
<a href="{% url 'invoice_detail' request.dealer.slug invoice.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
class="btn btn-sm btn-phoenix-success">{% trans "view" %}</a> class="btn btn-sm btn-phoenix-success">{% trans "view" %}</a>
</td> </td>
</tr> </tr>

View File

@ -447,7 +447,7 @@
<label class="form-label text-muted small mb-1">{{ _("Invoice") }}</label> <label class="form-label text-muted small mb-1">{{ _("Invoice") }}</label>
<p class="mb-0"> <p class="mb-0">
{% if saleorder.invoice %} {% if saleorder.invoice %}
<a href="{% url 'invoice_detail' saleorder.invoice.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug saleorder.invoice.pk %}"
target="_blank" target="_blank"
rel="noopener noreferrer"> rel="noopener noreferrer">
<p class="mb-0"> <p class="mb-0">

View File

@ -32,7 +32,7 @@
</td> </td>
<td class="align-middle product white-space-nowrap"> <td class="align-middle product white-space-nowrap">
{% if order.invoice %} {% if order.invoice %}
<a href="{% url 'invoice_detail' order.invoice.pk %}">{{ order.invoice }}</a> <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug order.invoice.pk %}">{{ order.invoice }}</a>
{% endif %} {% endif %}
</td> </td>
<td class="align-middle product white-space-nowrap py-0">{{ order.status }}</td> <td class="align-middle product white-space-nowrap py-0">{{ order.status }}</td>

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}
{{ _("Make Payment") }}
{% endblock title %}
{% block extra_css %}
<style>
.paid {
pointer-events: none;
opacity: 0.4;
}
</style>
{% endblock extra_css %}
{% block content %}
<div class="row {% if model.is_paid %}paid{% endif %}">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
{% if model.is_paid %}
<div class="card-header">{{ _("Payment Already Made") }}</div>
{% else %}
<div class="card-header">
<i class="fa-solid fa-hand-holding-dollar"></i> {{ _("Make Payment") }}
</div>
{% endif %}
<div class="card-body">
{% if model %}
<form method="post" action="{% url 'payment_create' request.dealer.slug model.pk %}">
{% endif %}
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-phoenix-primary">
<i class="fa-solid fa-floppy-disk"></i> {% trans 'Pay' %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -30,7 +30,7 @@
<td class="align-middle product white-space-nowrap py-0">{{ journal.je_number }}</td> <td class="align-middle product white-space-nowrap py-0">{{ journal.je_number }}</td>
{% if journal.ledger.invoicemodel %} {% if journal.ledger.invoicemodel %}
<td class="align-middle product white-space-nowrap py-0"> <td class="align-middle product white-space-nowrap py-0">
<a href="{% url 'invoice_detail' request.dealer.slug journal.ledger.invoicemodel.pk %}"><i class="fa-solid fa-receipt"></i> {{ journal.ledger.invoicemodel }}</a> <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug journal.ledger.invoicemodel.pk %}"><i class="fa-solid fa-receipt"></i> {{ journal.ledger.invoicemodel }}</a>
</td> </td>
{% elif journal.ledger.billmodel %} {% elif journal.ledger.billmodel %}
<td class="align-middle product white-space-nowrap py-0"> <td class="align-middle product white-space-nowrap py-0">

View File

@ -71,6 +71,40 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Vehicles Information -->
{% if sale_order.estimate %}
<div class="row mb-4">
<div class="col-12">
<h4>{% trans "Vehicles Information" %}</h4>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p>
<strong>{% trans "Make" %}:</strong> {{ sale_order.cars.0.id_car_make }}
<br>
<strong>{% trans "Model" %}:</strong> {{ sale_order.cars.0.id_car_model }}
<br>
<strong>{% trans "Year" %}:</strong> {{ sale_order.cars.0.year }}
<br>
<strong>{% trans "Vin" %}:</strong> {{ sale_order.cars.0.vin }}
<br>
<strong>{% trans "Status" %}: </strong><span class="badge bg-success">{{ sale_order.cars.0.status|capfirst }}</span>
</p>
</div>
<div class="col-md-6"></div>
</div>
{% if sale_order.estimate.notes %}
<div class="mt-3">
<strong>{% trans "Notes" %}:</strong>
<div class="border p-2 mt-1">{{ sale_order.estimate.notes|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Estimate Information --> <!-- Estimate Information -->
{% if sale_order.estimate %} {% if sale_order.estimate %}
<div class="row mb-4"> <div class="row mb-4">
@ -112,6 +146,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Invoice Information --> <!-- Invoice Information -->
{% if sale_order.invoice %} {% if sale_order.invoice %}
<div class="row mb-4"> <div class="row mb-4">
@ -258,39 +293,7 @@
</div> </div>
{% endif %} {% endif %}
<!-- Cars/Items --> <!-- Cars/Items -->
{% if sale_order.cars %}
<div class="row mb-4">
<div class="col-12">
<h4>{% trans "Vehicles" %}</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Make" %}</th>
<th>{% trans "Model" %}</th>
<th>{% trans "Year" %}</th>
<th>{% trans "VIN" %}</th>
<th>{% trans "Price" %}</th>
</tr>
</thead>
<tbody>
{% for car in sale_order.cars %}
<tr>
<td>{{ car.id_car_make }}</td>
<td>{{ car.id_car_model }}</td>
<td>{{ car.year }}</td>
<td>{{ car.vin }}</td>
<td>
<span class="currency">{{ CURRENCY }}</span>{{ car.finances.selling_price|floatformat:2 }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Cancellation Info --> <!-- Cancellation Info -->
{% if is_cancelled %} {% if is_cancelled %}
<div class="row mb-4"> <div class="row mb-4">
@ -327,7 +330,7 @@
class="btn btn-info ms-2">{% trans "View Full Estimate" %}</a> class="btn btn-info ms-2">{% trans "View Full Estimate" %}</a>
{% endif %} {% endif %}
{% if sale_order.invoice %} {% if sale_order.invoice %}
<a href="{% url 'invoice_detail' request.dealer.slug sale_order.invoice.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug sale_order.invoice.pk %}"
class="btn btn-info ms-2">{% trans "View Full Invoice" %}</a> class="btn btn-info ms-2">{% trans "View Full Invoice" %}</a>
{% endif %} {% endif %}
</div> </div>

View File

@ -66,7 +66,7 @@
<td class="align-middle white-space-nowrap invoice"> <td class="align-middle white-space-nowrap invoice">
{% if tx.invoice and perms.django_ledger.view_invoicemodel %} {% if tx.invoice and perms.django_ledger.view_invoicemodel %}
<p class="fw-bo text-body fs-9 mb-0"> <p class="fw-bo text-body fs-9 mb-0">
<a href="{% url 'invoice_detail' request.dealer.slug tx.invoice.uuid %}">{{ tx.invoice.invoice_number }}</a> <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug tx.invoice.uuid %}">{{ tx.invoice.invoice_number }}</a>
<br> <br>
{% if tx.invoice.is_draft %} {% if tx.invoice.is_draft %}
<span class="badge badge-phoenix badge-phoenix-warning">{{ tx.invoice.invoice_status }}</span> <span class="badge badge-phoenix badge-phoenix-warning">{{ tx.invoice.invoice_status }}</span>

View File

@ -0,0 +1,73 @@
{% load trans from i18n %}
{% load django_ledger %}
<form action="{% url 'django_ledger:invoice-update-items' entity_slug=entity_slug invoice_pk=invoice_model.uuid %}"
method="post">
<div class="columns is-multiline">
<div class="column is-12">
<h1 class="is-size-1">{% trans 'Invoice Items' %}</h1>
</div>
<div class="column is-12">
<div class="table-container">
{% csrf_token %}
{{ itemtxs_formset.non_form_errors }}
{{ itemtxs_formset.management_form }}
<table class="table is-fullwidth is-narrow is-striped is-bordered">
<thead>
<tr>
<th>{% trans 'Item' %}</th>
<th>{% trans 'Quantity' %}</th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'Available' %}</th>
<th>{% trans 'Total' %}</th>
<th>{% trans 'Delete' %}</th>
</tr>
</thead>
<tbody>
{% for f in itemtxs_formset %}
<tr>
<td>
{% for hidden_field in f.hidden_fields %}
{{ hidden_field }}
{% endfor %}
{{ f.item_model }}
{% if f.errors %}
{{ f.errors }}
{% endif %}
</td>
<td id="{{ f.instance.html_id_quantity }}">{{ f.quantity }}</td>
<td id="{{ f.instance.html_id_unit_cost }}">{{ f.unit_cost }}</td>
<td>{% if f.instance.item_model.for_inventory and not f.instance.invoice_model.is_approved %}
{{ f.instance.item_model.inventory_received }}
{% endif %}</td>
<td class="has-text-right" id="{{ f.instance.html_id_total_amount }}">
{% currency_symbol %}{{ f.instance.total_amount | currency_format }}</td>
<td class="has-text-centered">
{% if itemtxs_formset.can_delete %}
{{ f.DELETE }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th class="has-text-right">{% trans 'Total' %}</th>
<th class="has-text-right">{% currency_symbol %}{{ total_amount__sum | currency_format }}</th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="column is-12">
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
class="button is-primary">{% trans 'New Item' %}</a>
<button class="button is-primary">{% trans 'Save' %}</button>
</div>
</div>
</form>

View File

@ -24,7 +24,7 @@
</h3> </h3>
</div> </div>
<div class="card-body bg-light-subtle"> <div class="card-body bg-light-subtle">
<form class="row g-3 mb-9" method="post" class="form" novalidate> <form class="row g-3 mb-9" method="post" class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %} {% csrf_token %}
{{ redirect_field }} {{ redirect_field }}
{{ form.name|as_crispy_field }} {{ form.name|as_crispy_field }}
@ -32,7 +32,7 @@
{{ form.email|as_crispy_field }} {{ form.email|as_crispy_field }}
{{ form.phone_number|as_crispy_field }} {{ form.phone_number|as_crispy_field }}
{{ form.address|as_crispy_field }} {{ form.address|as_crispy_field }}
{{ form.image|as_crispy_field }} {{ form.logo|as_crispy_field }}
{{ form.group|as_crispy_field }} {{ form.group|as_crispy_field }}
{% for error in form.errors %}<div class="text-danger">{{ error }}</div>{% endfor %} {% for error in form.errors %}<div class="text-danger">{{ error }}</div>{% endfor %}
<hr class="my-2"> <hr class="my-2">

View File

@ -51,8 +51,8 @@
<div class="avatar avatar-tiny me-2"> <div class="avatar avatar-tiny me-2">
{% if user.logo %} {% if user.logo %}
<img class="avatar-img rounded-circle" <img class="avatar-img rounded-circle"
src="{{ user.thumbnail.url }}" src="{{user.thumbnail.url}}"
onerror="this.src='/static/img/brand/brand-logo.png'" onerror="this.src='/static/user-logo.png'"
alt="Logo"> alt="Logo">
{% endif %} {% endif %}
</div> </div>