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
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.auth import AuthMiddlewareStack
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(
{
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
}
)
# application = ProtocolTypeRouter(
# {
# "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.urls.i18n import i18n_patterns
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
from schema_graph.views import Schema
# from two_factor.urls import urlpatterns as tf_urls
@ -30,6 +30,7 @@ urlpatterns += i18n_patterns(
path("plans/", include("plans.urls")),
path("schema/", Schema.as_view()),
path("tours/", include("tours.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.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
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_finances = forms.ModelMultipleChoiceField(
queryset=AdditionalServices.objects.all(),
widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
required=False,
)
# additional_finances = forms.ModelMultipleChoiceField(
# queryset=AdditionalServices.objects.all(),
# widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}),
# required=False,
# )
class Meta:
model = CarFinance
exclude = [
"car",
"profit_margin",
"vat_amount",
"total",
"additional_services",
]
fields = ["cost_price","marked_price"]
def save(self, commit=True):
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()
return instance
@ -1362,27 +1359,27 @@ class SaleOrderForm(forms.ModelForm):
class Meta:
model = SaleOrder
fields = [
"customer",
# "customer",
"expected_delivery_date",
"estimate",
"opportunity",
# "estimate",
# "opportunity",
"comments",
"order_date",
"status",
# "order_date",
# "status",
]
widgets = {
"expected_delivery_date": forms.DateInput(
attrs={"type": "date", "label": _("Expected Delivery Date")}
),
"order_date": forms.DateInput(
attrs={"type": "date", "label": _("Order Date")}
),
"customer": forms.Select(
attrs={
"class": "form-control",
"label": _("Customer"),
}
),
# "order_date": forms.DateInput(
# attrs={"type": "date", "label": _("Order Date")}
# ),
# "customer": forms.Select(
# attrs={
# "class": "form-control",
# "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 time
# from django.http import Http404, HttpResponseForbidden
# from django.shortcuts import redirect
@ -44,33 +45,33 @@ logger = logging.getLogger("user_activity")
# return request.META.get("REMOTE_ADDR")
class InjectParamsMiddleware:
"""
Middleware to add processed user-related parameters to the request object.
# class InjectParamsMiddleware:
# """
# Middleware to add processed user-related parameters to the request object.
This middleware processes incoming requests to extract and enhance user
information, specifically linking user context such as `dealer` to the
request. It allows subsequent views and middlewares to access these enriched
request parameters with ease.
# This middleware processes incoming requests to extract and enhance user
# information, specifically linking user context such as `dealer` to the
# request. It allows subsequent views and middlewares to access these enriched
# request parameters with ease.
:ivar get_response: The callable to get the next middleware or view response.
:type get_response: Callable
"""
# :ivar get_response: The callable to get the next middleware or view response.
# :type get_response: Callable
# """
def __init__(self, get_response):
self.get_response = get_response
# def __init__(self, get_response):
# self.get_response = get_response
def __call__(self, request):
try:
if request.user.is_authenticated:
request.dealer = get_user_type(request)
request.entity = request.dealer.entity
else:
request.dealer = None
except Exception:
pass
response = self.get_response(request)
return response
# def __call__(self, request):
# try:
# if request.user.is_authenticated:
# request.dealer = get_user_type(request)
# request.entity = request.dealer.entity
# else:
# request.dealer = None
# except Exception:
# pass
# response = self.get_response(request)
# return response
class InjectDealerMiddleware:
@ -93,6 +94,7 @@ class InjectDealerMiddleware:
def __call__(self, request):
try:
start = time.time()
if request.user.is_authenticated:
request.is_dealer = False
request.is_staff = False
@ -103,6 +105,7 @@ class InjectDealerMiddleware:
if hasattr(request.user, "dealer"):
request.is_dealer = True
request.dealer = request.user.dealer
elif hasattr(request.user, "staffmember"):
request.is_staff = True
request.staff = request.user.staffmember.staff
@ -120,6 +123,7 @@ class InjectDealerMiddleware:
request.is_inventory = True
request.entity = request.dealer.entity
request.admin = request.dealer.entity.admin
print("\033[92m⏱ Middleware time:", time.time() - start, "\033[0m")
except Exception:
pass
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 django.core.serializers.json
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
('name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=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)),
('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')),
('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')),
('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')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('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')),
('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')),
('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')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('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')),
('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')),
('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')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('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')),
('email', models.EmailField(max_length=255, verbose_name='Email 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')),
('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')),

View File

@ -223,7 +223,7 @@ class CarMake(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, 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)
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)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
@ -719,6 +719,12 @@ class Car(Base):
return active_reservations.exists()
@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):
try:
return all(
@ -1174,7 +1180,13 @@ class Dealer(models.Model, LocalizedNameMixin):
blank=True,
null=True,
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(
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")
)
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(
source="logo",
@ -1497,6 +1509,12 @@ class Customer(models.Model):
image = models.ImageField(
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"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(
@ -1639,7 +1657,13 @@ class Organization(models.Model, LocalizedNameMixin):
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
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"))
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"))
address = models.CharField(max_length=200, verbose_name=_("Address"))
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"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
@ -2776,7 +2806,7 @@ class SaleOrder(models.Model):
if self.invoice:
# Check if get_itemtxs_data returns data before proceeding
# 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:
return 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})"
@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:
return []
@ -3310,12 +3340,18 @@ class ExtraInfo(models.Model):
if is_dealer:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
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:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id=staff.pk,
@ -3330,7 +3366,7 @@ class ExtraInfo(models.Model):
]
@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:
return []
@ -3339,17 +3375,22 @@ class ExtraInfo(models.Model):
if is_dealer:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
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:
qs = cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=related_content_type,
related_object_id=staff.pk,
)
print(qs[0].content_object.invoicemodel_set.first())
return [
x.content_object.invoicemodel_set.first()
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
from .models import Dealer
from django.core.exceptions import ImproperlyConfigured, ValidationError
@ -19,7 +20,7 @@ from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
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_ledger.forms.purchase_order import (
ApprovedPurchaseOrderModelUpdateForm,
@ -35,6 +36,11 @@ from django.views.generic.edit import UpdateView
from django.views.generic.base import RedirectView
from django.views.generic.list import ListView
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__)
@ -315,41 +321,68 @@ class BasePurchaseOrderActionActionView(
f"User {user_username} attempting to call action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
)
try:
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
# --- Single-line log for successful action ---
logger.info(
f"User {user_username} successfully executed action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk}."
)
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}"
)
print(self.action_name)
if self.action_name == "mark_as_fulfilled":
try:
if po_model.can_fulfill():
po_model.mark_as_fulfilled()
# po_model.date_fulfilled = timezone.now()
po_model.save()
messages.add_message(
request,
message="PO marked as fulfilled successfully.",
level=messages.SUCCESS,
)
logger.info(
f"User {user_username} successfully executed action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk}."
)
except Exception as e:
messages.add_message(
request,
message=f"Failed to mark PO {po_model.po_number} as fulfilled. {e}",
level=messages.ERROR,
)
logger.warning(
f"User {user_username} encountered an exception "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
else:
try:
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
logger.info(
f"User {user_username} successfully executed action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk}."
)
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
@ -731,3 +764,212 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
entity_slug=self.kwargs["entity_slug"],
)
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,
coa_model=coa,
)
instance.item_model = inventory
inventory.save()
# inventory = entity.create_item_inventory(
# name=instance.vin,
# uom_model=uom,
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
# )
instance.item_model = inventory
inventory.additional_info = {}
inventory.additional_info.update({"car_info": instance.to_dict()})
inventory.save()
else:
instance.item_model.additional_info.update({"car_info": instance.to_dict()})
instance.item_model.save()
# inventory.additional_info = {}
# inventory.additional_info.update({"car_info": instance.to_dict()})
# inventory.save()
# else:
# instance.item_model.additional_info.update({"car_info": instance.to_dict()})
# instance.item_model.save()
# # 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
if not isinstance(instance.car.item_model.additional_info, dict):
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(
{
"additional_services": [
service.to_dict() for service in instance.additional_services.all()
]
}
)
# instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
# instance.car.item_model.additional_info.update(
# {
# "additional_services": [
# service.to_dict() for service in instance.additional_services.all()
# ]
# }
# )
instance.car.item_model.save()
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)
def create_po_fulfilled_notification(sender, instance, created, **kwargs):
if instance.po_status == "fulfilled":
dealer = models.Dealer.objects.get(entity=instance.entity)
accountants = (
models.CustomGroup.objects.filter(dealer=dealer, name="Inventory")
.first()
.group.user_set.exclude(email=dealer.user.email)
.distinct()
)
for accountant in accountants:
models.Notification.objects.create(
user=accountant,
message=f"""
New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""",
)
# @receiver(post_save, sender=PurchaseOrderModel)
# def create_po_fulfilled_notification(sender, instance, created, **kwargs):
# if instance.po_status == "fulfilled":
# dealer = models.Dealer.objects.get(entity=instance.entity)
# accountants = (
# models.CustomGroup.objects.filter(dealer=dealer, name="Inventory")
# .first()
# .group.user_set.exclude(email=dealer.user.email)
# .distinct()
# )
# for accountant in accountants:
# models.Notification.objects.create(
# user=accountant,
# message=f"""
# New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
# <a href="{instance.get_absolute_url()}" target="_blank">View</a>
# """,
# )
@receiver(post_save, sender=models.Car)
@ -1005,7 +1006,7 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled():
dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer,
groups__customgroup__dealer=dealer,
groups__customgroup__name__in=["Manager", "Inventory"],
).distinct()
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),
object_id=instance.pk,
).first()
if not recipient:
return
models.Notification.objects.create(
user=recipient.related_object.user,
message=f"""

View File

@ -4,9 +4,10 @@ from django_ledger.io import roles
from django_q.tasks import async_task
from django.core.mail import send_mail
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 django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -220,7 +221,7 @@ def create_coa_accounts(instance):
"role": roles.LIABILITY_CL_TAXES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for LIABILITY_CL_TAXES_PAYABLE
"default": False, # Default for LIABILITY_CL_TAXES_PAYABLE
},
{
"code": "2070",
@ -239,6 +240,14 @@ def create_coa_accounts(instance):
"locked": False,
"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",
"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.set_password(password)
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")
user.groups.add(group)
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)
StaffMember.objects.create(user=user)
Dealer.objects.create(
dealer = Dealer.objects.create(
user=user,
name=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,
address=address,
)
return dealer
# 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)
def bill_item_formset_table(context, item_formset):
bill = BillModel.objects.get(uuid=context["view"].kwargs["bill_pk"])
for item in item_formset:
if item:
item.initial["quantity"] = item.instance.po_quantity
@ -484,6 +485,7 @@ def bill_item_formset_table(context, item_formset):
return {
"dealer_slug": context["view"].kwargs["dealer_slug"],
"entity_slug": context["view"].kwargs["entity_slug"],
"bill": bill,
"bill_pk": context["view"].kwargs["bill_pk"],
"total_amount__sum": context["total_amount__sum"],
"item_formset": item_formset,
@ -672,3 +674,12 @@ def count_checked(permissions, group_permission_ids):
# def count_checked(permissions, group_permission_ids):
# """Count how many permissions are checked from the allowed list"""
# 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(),
name="customer_detail",
),
path(
"<slug:dealer_slug>/customers/<slug:slug>/add-note/",
views.add_note_to_customer,
name="add_note_to_customer",
),
# path(
# "<slug:dealer_slug>/customers/<slug:slug>/add-note/",
# views.add_note_to_customer,
# name="add_note_to_customer",
# ),
path(
"<slug:dealer_slug>/customers/<slug:slug>/update/",
views.CustomerUpdateView.as_view(),
@ -198,11 +198,11 @@ urlpatterns = [
views.lead_transfer,
name="lead_transfer",
),
path(
"<slug:dealer_slug>/crm/opportunities/<slug:slug>/add_note/",
views.add_note_to_opportunity,
name="add_note_to_opportunity",
),
# path(
# "<slug:dealer_slug>/crm/opportunities/<slug:slug>/add_note/",
# views.add_note_to_opportunity,
# name="add_note_to_opportunity",
# ),
path(
"<slug:dealer_slug>/crm/opportunities/create/",
views.OpportunityCreateView.as_view(),
@ -836,10 +836,15 @@ urlpatterns = [
name="invoice_create",
),
path(
"<slug:dealer_slug>/sales/invoices/<uuid:pk>/",
"<slug:dealer_slug>/sales/<slug:entity_slug>/invoices/<uuid:pk>/",
views.InvoiceDetailView.as_view(),
name="invoice_detail",
),
# path(
# "<slug:dealer_slug>/sales/<slug:entity_slug>/invoices/<uuid:pk>/update",
# views.InvoiceDetailView.as_view(),
# name="invoice_update",
# ),
path(
"<slug:dealer_slug>/sales/invoices/<uuid:pk>/preview/",
views.InvoicePreviewView.as_view(),
@ -876,7 +881,17 @@ urlpatterns = [
views.PaymentCreateView,
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(
"<slug:dealer_slug>/sales/payments/<uuid:pk>/payment_details/",
views.PaymentDetailView,

View File

@ -14,6 +14,7 @@ from django_q.tasks import async_task
from django.core.mail import send_mail
from plans.models import AbstractOrder
from django_ledger.models import (
EstimateModel,
InvoiceModel,
BillModel,
VendorModel,
@ -25,8 +26,9 @@ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django_ledger.models.transactions import TransactionModel
from django_ledger.models.journal_entry import JournalEntryModel
from django.db import transaction
import logging
from django_ledger.io import roles
logger = logging.getLogger(__name__)
@ -452,30 +454,35 @@ def get_financial_values(model):
}
def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
"""
Processes and applies a payment for a specified invoice. This function calculates
finance details, handles associated account transactions, and updates the invoice
status accordingly.
# def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
# """
# Processes and applies a payment for a specified invoice. This function calculates
# finance details, handles associated account transactions, and updates the invoice
# status accordingly.
:param dealer: Dealer object responsible for processing the payment
:type dealer: Dealer
:param entity: Entity object associated with the invoice and payment
:type entity: Entity
:param invoice: The invoice object for which the payment is being made
:type invoice: Invoice
:param amount: The amount being paid towards the invoice
:type amount: Decimal
:param payment_method: The payment method used for the transaction
:type payment_method: str
:return: None
"""
calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data()
# :param dealer: Dealer object responsible for processing the payment
# :type dealer: Dealer
# :param entity: Entity object associated with the invoice and payment
# :type entity: Entity
# :param invoice: The invoice object for which the payment is being made
# :type invoice: Invoice
# :param amount: The amount being paid towards the invoice
# :type amount: Decimal
# :param payment_method: The payment method used for the transaction
# :type payment_method: str
# :return: None
# """
# calculator = CarFinanceCalculator(invoice)
# finance_data = calculator.get_finance_data()
handle_account_process(invoice, amount, finance_data)
invoice.make_payment(amount)
invoice.save()
# handle_account_process(invoice, amount, finance_data)
# if invoice.can_migrate():
# 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):
@ -996,16 +1003,25 @@ class CarFinanceCalculator:
ADDITIONAL_SERVICES_KEY = "additional_services"
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.vat_rate = self._get_vat_rate()
self.item_transactions = self._get_item_transactions()
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,
)
# self.additional_services = self._get_additional_services()
def _get_vat_rate(self):
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")
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):
return self.model.get_itemtxs_data()[0].all()
def get_items(self):
return self._get_item_transactions()
@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_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))
car = item.item_model.car
unit_price = Decimal(car.finances.marked_price)
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"),
"vin": car.vin, #car_info.get("vin"),
"make": car.id_car_make ,#car_info.get("make"),
"model": car.id_car_model ,#car_info.get("model"),
"year": car.year ,# car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim ,# car_info.get("trim"),
"mileage": car.mileage ,# car_info.get("mileage"),
"cost_price": car.finances.cost_price,
"selling_price": car.finances.selling_price,
"marked_price": car.finances.marked_price,
"discount": car.finances.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
),
"total_vat": car.finances.total_vat,
"additional_services": car.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))
Decimal(item.item_model.car.finances.marked_price)
for item in self.item_transactions
)
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 = 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)
@ -1092,11 +1093,11 @@ class CarFinanceCalculator:
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_price": round(total_price_discounted, 2),
"total_vat_amount": round(total_vat_amount, 2),
"total_discount": round(Decimal(total_discount)),
"total_additionals": round(total_additionals, 2), # total_additionals,
"total_additionals": round(total_additionals, 2),
"grand_total": round(
total_price_discounted + total_vat_amount + total_additionals, 2
),
@ -1116,9 +1117,167 @@ class CarFinanceCalculator:
"total_discount": totals["total_discount"],
"total_additionals": totals["total_additionals"],
"grand_total": totals["grand_total"],
"additionals": self.additional_services,
"additionals": self._get_additional_services(),
"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):
@ -1175,134 +1334,224 @@ def get_local_name(self):
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,
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
Records the customer payment (`make_payment`) and posts the full
accounting (sales + VAT + COGS + Inventory).
"""
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()
invoice.make_payment(amount)
invoice.save()
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()
_post_sale_and_cogs(invoice, dealer)
# make_account = entity.get_all_accounts().filter(name=car.id_car_make.name,role=roles.COGS).first()
# if not make_account:
# last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first()
# if len(last_account.code) == 4:
# code = f"{int(last_account.code)}{1:03d}"
# elif len(last_account.code) > 4:
# code = f"{int(last_account.code)+1}"
def _post_sale_and_cogs(invoice, dealer):
"""
For every car line on the invoice:
1) Cash / A-R / VAT / Revenue journal
2) COGS / Inventory journal
"""
entity = invoice.ledger.entity
calc = CarFinanceCalculator(invoice)
data = calc.get_finance_data()
# make_account = entity.create_account(
# name=car.id_car_make.name,
# code=code,
# role=roles.COGS,
# coa_model=coa,
# balance_type="debit",
# active=True
# )
cash_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
ar_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
vat_acc = entity.get_all_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
car_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
add_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
cogs_acc = entity.get_all_accounts().filter(role_default=True, role=roles.COGS).first()
inv_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
# # get or create additional services account
# additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first()
# if not additional_services_account:
# last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first()
# if len(last_account.code) == 4:
# code = f"{int(last_account.code)}{1:03d}"
# elif len(last_account.code) > 4:
# code = f"{int(last_account.code)+1}"
for car_data in data['cars']:
car = invoice.get_itemtxs_data()[0].filter(
item_model__car__vin=car_data['vin']
).first().item_model.car
qty = Decimal(car_data['quantity'])
# additional_services_account = entity.create_account(
# name="Additional Services",
# code=code,
# role=roles.COGS,
# coa_model=coa,
# balance_type="debit",
# active=True
# )
net_car_price = Decimal(car_data['total'])
net_add_price = Decimal(data['total_additionals'])
vat_amount = Decimal(data['total_vat_amount']) * qty
grand_total = net_car_price + net_add_price + vat_amount
cost_total = Decimal(car_data['cost_price']) * qty
# 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)
journal = JournalEntryModel.objects.create(
posted=False,
description=f"Payment for Invoice {invoice.invoice_number}",
je_sale = JournalEntryModel.objects.create(
ledger=invoice.ledger,
description=f"Sale {car.vin}",
origin=f"Invoice {invoice.invoice_number}",
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(
journal_entry=journal,
account=cash_account,
amount=Decimal(finance_data.get("grand_total")),
tx_type="debit",
description="",
journal_entry=je_sale,
account=vat_acc,
amount=vat_amount,
tx_type='credit'
)
# Cr Sales Car
TransactionModel.objects.create(
journal_entry=journal,
account=revenue_account,
amount=Decimal(finance_data.get("grand_total")),
tx_type="credit",
description="",
journal_entry=je_sale,
account=car_rev,
amount=net_car_price,
tx_type='credit'
)
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,
if net_add_price > 0:
# Cr Sales Additional Services
TransactionModel.objects.create(
journal_entry=je_sale,
account=add_rev,
amount=net_add_price,
tx_type='credit'
)
# ------------------------------------------------------------------
# 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.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(
# 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.decorators import permission_required
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 (
View,
ListView,
@ -145,6 +145,7 @@ from .override import (
BillModelUpdateView as BillModelUpdateViewBase,
BaseBillActionView as BaseBillActionViewBase,
InventoryListView as InventoryListViewBase,
InvoiceModelUpdateView as InvoiceModelUpdateViewBase,
)
from django_ledger.models import (
@ -180,8 +181,6 @@ from django_ledger.views.mixins import (
)
# Other
from plans.models import Plan
from . import models, forms, tables
from django_tables2 import SingleTableView
from django_tables2.export.views import ExportMixin
@ -1145,7 +1144,6 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
context["stats"] = {
"all": cars.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
cars = models.Car.objects.filter(dealer=request.dealer)
print(cars)
# Count for total, reserved, showroom, and unreserved cars
total_cars = cars.count()
reserved_cars = models.CarReservation.objects.count()
@ -1517,13 +1515,13 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
context["car"] = self.car
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form
# def get_form(self, form_class=None):
# form = super().get_form(form_class)
# dealer = get_user_type(self.request)
# form.fields[
# "additional_finances"
# ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
# return form
class CarFinanceUpdateView(
@ -1570,21 +1568,21 @@ class CarFinanceUpdateView(
kwargs["instance"] = self.get_object()
return kwargs
def get_initial(self):
initial = super().get_initial()
instance = self.get_object()
dealer = get_user_type(self.request)
selected_items = instance.additional_services.filter(dealer=dealer)
initial["additional_finances"] = selected_items
return initial
# def get_initial(self):
# initial = super().get_initial()
# instance = self.get_object()
# dealer = get_user_type(self.request)
# selected_items = instance.additional_services.filter(dealer=dealer)
# initial["additional_finances"] = selected_items
# return initial
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form
# def get_form(self, form_class=None):
# form = super().get_form(form_class)
# dealer = get_user_type(self.request)
# form.fields[
# "additional_finances"
# ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
# return form
class CarUpdateView(
@ -2405,7 +2403,6 @@ class CustomerCreateView(
success_message = "Customer created successfully"
def form_valid(self, form):
sleep(5)
if customer := models.Customer.objects.filter(
email=form.instance.email
).first():
@ -4328,12 +4325,12 @@ def sales_list_view(request, dealer_slug):
qs = []
try:
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:
qs = models.ExtraInfo.get_sale_orders(staff=staff)
qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer)
except Exception as e:
print(e)
print(qs[0])
# query = request.GET.get('q')
# # if query:
# # qs = qs.filter(
@ -4418,7 +4415,6 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
staff = getattr(self.request.user.staffmember, "staff", None)
if any(
[
@ -4431,16 +4427,19 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff),
)
print(qs)
).union(models.ExtraInfo.objects.filter(
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:
qs = models.ExtraInfo.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
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
return context
@ -4579,6 +4578,7 @@ def create_estimate(request, dealer_slug, slug=None):
).all()
for i in car_instance[: int(quantities[0])]:
print(i)
items_txs.append(
{
"item_number": i.item_model.item_number,
@ -4641,11 +4641,11 @@ def create_estimate(request, dealer_slug, slug=None):
opportunity.estimate = estimate
opportunity.save()
if staff := getattr(request.user.staffmember, "staff", None):
if request.is_staff:
models.ExtraInfo.objects.create(
dealer=dealer,
content_object=estimate,
related_object=staff,
related_object=request.staff,
created_by=request.user,
)
else:
@ -4839,21 +4839,21 @@ def create_sale_order(request, dealer_slug, pk):
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
form = forms.SaleOrderForm()
customer = estimate.customer.customer_set.first()
form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
form.initial["estimate"] = estimate
form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
form.initial["customer"] = customer
if hasattr(estimate, "opportunity"):
form.initial["opportunity"] = estimate.opportunity
else:
form.fields["opportunity"].widget = HiddenInput()
# customer = estimate.customer.customer_set.first()
# form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
# form.initial["estimate"] = estimate
# form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
# form.initial["customer"] = customer
# if hasattr(estimate, "opportunity"):
# form.initial["opportunity"] = estimate.opportunity
# else:
# form.fields["opportunity"].widget = HiddenInput()
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
return render(
request,
"sales/estimates/sale_order_form1.html",
"sales/estimates/sale_order_form.html",
{"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.save()
messages.success(request, "Discount updated successfully")
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"]
)
car.finances.save()
messages.success(request, "Additional Finances updated successfully")
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
@ -5147,9 +5148,9 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
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:
qs = models.ExtraInfo.get_invoices(staff=staff)
qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer)
except Exception as e:
print(e)
@ -5285,7 +5286,7 @@ class ApprovedInvoiceModelUpdateFormView(
def get_success_url(self):
return reverse_lazy(
"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):
return reverse_lazy(
"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):
@ -5341,7 +5342,7 @@ class PaidInvoiceModelUpdateFormView(
if invoice.get_amount_open() > 0:
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:
invoice.post_ledger()
invoice.save()
@ -5373,12 +5374,12 @@ def invoice_mark_as(request, dealer_slug, pk):
if mark and mark == "accept":
if not invoice.can_approve():
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(
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
)
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
@ -5441,7 +5442,7 @@ def invoice_create(request, dealer_slug, pk):
estimate.save()
invoice.save()
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:
print(form.errors)
form = forms.InvoiceModelCreateForm(
@ -5501,6 +5502,32 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
# 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
@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)
invoice = InvoiceModel.objects.filter(pk=pk).first()
bill = BillModel.objects.filter(pk=pk).first()
model = invoice if invoice else bill
# bill = BillModel.objects.filter(pk=pk).first()
model = invoice
entity = dealer.entity
form = forms.PaymentForm()
breakpoint()
if request.method == "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_username = (
request.user.username if request.user.is_authenticated else "anonymous"
@ -5546,19 +5573,19 @@ def PaymentCreateView(request, dealer_slug, pk):
if form.is_valid():
amount = form.cleaned_data.get("amount")
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")
redirect_url = "invoice_detail" if invoice else "bill_detail"
model = invoice if invoice else bill
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
if not model.is_approved():
model.mark_as_approved(user_model=entity.admin)
if model.amount_paid == model.amount_due:
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:
messages.error(request, _("Amount exceeds due amount"))
return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk)
return response
try:
if invoice:
@ -5566,14 +5593,14 @@ def PaymentCreateView(request, dealer_slug, pk):
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}."
)
elif bill:
set_bill_payment(dealer, entity, bill, amount, payment_method)
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}."
)
# elif bill:
# set_bill_payment(dealer, entity, bill, amount, payment_method)
# 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}."
# )
messages.success(request, _("Payment created successfully"))
return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk)
return response
except Exception as e:
logger.error(
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.fields["invoice"].widget = HiddenInput()
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,
)
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
@ -6577,41 +6604,43 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None):
)
messages.success(request, _("Email Draft successfully"))
try:
if getattr(lead, "opportunity", None):
# Log success when opportunity exists and redirecting
logger.info(
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."
)
response = HttpResponse(
redirect(
"opportunity_detail",
dealer_slug=dealer_slug,
slug=lead.opportunity.slug,
)
)
response["HX-Redirect"] = reverse(
"opportunity_detail", args=[lead.opportunity.slug]
)
else:
# Log success when no opportunity and redirecting to lead detail
logger.info(
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(
"lead_detail", dealer_slug=dealer_slug, slug=lead.slug
)
return response
except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (Draft status) ---
logger.info(
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)
# try:
# if getattr(lead, "opportunity", None):
# # Log success when opportunity exists and redirecting
# logger.info(
# 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."
# )
# # response = HttpResponse(
# # redirect(
# # "opportunity_detail",
# # dealer_slug=dealer_slug,
# # slug=lead.opportunity.slug,
# # )
# # )
# # response["HX-Redirect"] = reverse(
# # "opportunity_detail", args=[lead.opportunity.slug]
# # )
# else:
# # Log success when no opportunity and redirecting to lead detail
# logger.info(
# 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(
# # "lead_detail", dealer_slug=dealer_slug, slug=lead.slug
# # )
# return response
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
# # --- Log when Lead.opportunity does not exist (Draft status) ---
# logger.info(
# 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 response
# return redirect("lead_list", dealer_slug=dealer.slug)
if request.method == "POST":
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,
)
messages.success(request, _("Email sent successfully"))
try:
if lead.opportunity:
# Log success when opportunity exists and redirecting after sending email
logger.info(
f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
)
return redirect(
"opportunity_detail",
dealer_slug=dealer_slug,
slug=lead.opportunity.slug,
)
except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (POST request for sending) ---
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 redirect("lead_list", dealer_slug=dealer_slug)
response = HttpResponse()
response["HX-Refresh"] = "true"
return response
# try:
# if lead.opportunity:
# # Log success when opportunity exists and redirecting after sending email
# logger.info(
# f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
# )
# return response
# # return redirect(
# # "opportunity_detail",
# # dealer_slug=dealer_slug,
# # slug=lead.opportunity.slug,
# # )
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
# # --- Log when Lead.opportunity does not exist (POST request for sending) ---
# 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"""
السلام عليكم
Dear {lead.full_name},
@ -6745,14 +6779,18 @@ class OpportunityCreateView(
def get_form(self, form_class=None):
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.fields["car"].queryset = models.Car.objects.filter(
dealer=dealer, status="available", finances__marked_price__gt=0
)
form.fields["lead"].queryset = models.Lead.objects.filter(
dealer=dealer, staff=staff
)
if self.request.is_dealer:
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
def get_success_url(self):
@ -9304,7 +9342,7 @@ def sse_stream(request):
)
last_id = notification.id
sleep(2)
sleep(3)
response = StreamingHttpResponse(event_stream(), content_type="text/event-stream")
response["Cache-Control"] = "no-cache"
@ -10258,6 +10296,12 @@ def upload_cars(request, dealer_slug, pk=None):
csv_data = io.StringIO(file_content)
reader = csv.DictReader(csv_data)
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:
# Log VIN decoding and initial validation for each row
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">
{% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="user-style-rtl">
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
{% else %}
<link href="{% static 'css/theme.min.css' %}"
type="text/css"

View File

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

View File

@ -2,8 +2,14 @@
{% load static %}
{% load django_ledger %}
{% 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">
<!-- Page Header -->
<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="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto flex-1">
<h3 class="fw-bolder mb-2">{{ lead.first_name }} {{ lead.last_name }}</h3>
{% if lead.staff %}
<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 %}
<h3 class="fw-bolder mb-2">{{ lead.first_name|capfirst }} {{ lead.last_name|capfirst }}</h3>
<p>{{ lead.email|capfirst }}</p>
</div>
<div class="col-6 col-sm-auto flex-1">
<h5 class="text-body-highlight mb-0 text-end">
@ -107,6 +101,34 @@
</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-body">
<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">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3>
{% 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">
<span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %}
</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 %}
</div>
<div>
@ -791,21 +824,7 @@
</div>
{% include 'modal/delete_modal.html' %}
<!-- add update Modal -->
{% comment %} <div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
<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 %}
{% include "components/email_modal.html" %}
<!-- task Modal -->
{% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
<!-- note Modal -->

View File

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

View File

@ -8,7 +8,8 @@
<div class="card email-content">
<h5 class="card-header">Send Mail</h5>
<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 %}"
method="post">
{% csrf_token %}
@ -47,7 +48,7 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<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"
hx-push-url='false'
hx-include="#message,#subject,#to"

View File

@ -180,7 +180,7 @@
</div>
{% if opportunity.estimate.invoice %}
<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 %}
<p>{{ _("No Invoice") }}</p>
{% endif %}
@ -819,12 +819,25 @@
<h2 class="mb-4">Emails</h2>
{% if perms.inventory.change_opportunity %}
<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">
<span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %}
</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>
{% endif %}
<div>
@ -843,15 +856,7 @@
</li>
</ul>
</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-pane fade show active"
id="tab-mail"
@ -864,13 +869,7 @@
<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"
data-bulk-select='{"body":"all-email-table-body"}' />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col"
data-sort="subject"
@ -884,11 +883,7 @@
<th class="sort align-middle text-start text-uppercase"
scope="col"
data-sort="date"
style="min-width:165px">Date</th>
<th class="sort align-middle pe-0 text-uppercase"
scope="col"
style="width:15%;
min-width:100px">Action</th>
style="min-width:15px">Date</th>
<th class="sort align-middle text-end text-uppercase"
scope="col"
data-sort="status"
@ -899,22 +894,14 @@
<tbody class="list" id="all-email-table-body">
{% for email in opportunity.lead.get_emails %}
<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">
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
<div class="fs-10 d-block">{{ email.to_email }}</div>
</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="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">
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
</td>
@ -1103,6 +1090,9 @@
</div>
</div>
</div>
<!-- email Modal -->
{% include "components/email_modal.html" %}
<!-- task Modal -->
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- note Modal -->

View File

@ -123,13 +123,13 @@
<button type="reset" class="btn btn-phoenix-danger px-4">
<span class="fas fa-redo me-1"></span>{% trans "Reset" %}
</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" %}
{% else %}
<span class="fas fa-plus me-1"></span>{% trans "Create" %}
{% endif %}
</button>
{% endif %}
</button>
</div>
</form>
</div>

View File

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

View File

@ -30,7 +30,7 @@
<td class="align-middle product white-space-nowrap px-1">
{% if ledger.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 %}
{% elif ledger.billmodel %}
{% if perms.django_ledger.view_billmodel %}

View File

@ -77,6 +77,16 @@
</li>
<script>
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 seenNotificationIds = new Set();
@ -115,7 +125,10 @@
updateCounter(unreadCount);
connectSSE();
setTimeout(() => {
connectSSE();
}, 5000);
}
})
.catch(error => {
@ -129,7 +142,7 @@
eventSource.close();
}
eventSource = new EventSource("{% url 'sse_stream' %}?last_id=" + lastNotificationId);
eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId);
eventSource.addEventListener('notification', function(e) {
try {
@ -152,6 +165,11 @@
console.log("Audio play failed - may need user interaction first:", e);
});
Toast.fire({
icon: 'info',
html:`${data.message}`
});
} catch (error) {
console.error('Error processing notification:', error);
}
@ -159,7 +177,8 @@
eventSource.addEventListener('error', function(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>
{% endif %}
{% 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"
type="button"><i class="fa-solid fa-receipt"></i>
{{ _("View Invoice") }}</a>
@ -286,7 +286,7 @@
type="button"
data-bs-toggle="modal"
data-bs-target="#additionalModal">
<span class="fas fa-plus me-1"></span>{{ _("Add") }}
<span class="fas fa-plus me-1"></span>{{ _("") }}
</button>
</td>
</tr>

View File

@ -1,248 +1,35 @@
{% extends 'base.html' %}
{% load i18n static %}
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}
{% trans 'Sale Order' %}
<h1>{% trans 'Sale Order' %}</h1>
{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static 'flags/sprite.css' %}" />
<div class="row">
<div class="row mb-3">
<div class="col-sm-6 col-md-8">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if customer.created %}
{{ _("Edit Sale Order") }}
{% else %}
{{ _("Add Sale Order") }}
{% endif %}
<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">
{% trans 'Sale Order' %}
</h3>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-xl-12 col-xxl-12">
<div class="px-xl-12">
<div class="row mx-0 mx-sm-3 mx-lg-0 px-lg-0">
<div class="col-sm-12 col-xxl-6 py-3">
<table class="w-100 table-stats ">
<tr>
<th></th>
<th></th>
<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 class="card-body bg-light-subtle">
<form method="post" action="">
{% csrf_token %}
{{ form|crispy }}
<hr class="my-2">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-lg btn-phoenix-success md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button>
<a href="{{ request.META.HTTP_REFERER }}"
class="btn btn-lg btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</div>
<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>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
<!---->
{% endblock content %}

View File

@ -121,7 +121,10 @@
{% endif %}
{% if invoice.invoice_status == 'approved' %}
{% 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 %}"
{% 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>
{% endif %}
{% endif %}

View File

@ -64,7 +64,7 @@
</td>
<td class="align-middle product white-space-nowrap">{{ invoice.created }}</td>
<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">
<i class="fa-regular fa-eye me-1"></i>
{% 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="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>
</td>
</tr>

View File

@ -447,7 +447,7 @@
<label class="form-label text-muted small mb-1">{{ _("Invoice") }}</label>
<p class="mb-0">
{% 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"
rel="noopener noreferrer">
<p class="mb-0">

View File

@ -32,7 +32,7 @@
</td>
<td class="align-middle product white-space-nowrap">
{% 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 %}
</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>
{% if journal.ledger.invoicemodel %}
<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>
{% elif journal.ledger.billmodel %}
<td class="align-middle product white-space-nowrap py-0">

View File

@ -71,6 +71,40 @@
</div>
{% endif %}
</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 -->
{% if sale_order.estimate %}
<div class="row mb-4">
@ -112,6 +146,7 @@
</div>
</div>
{% endif %}
<!-- Invoice Information -->
{% if sale_order.invoice %}
<div class="row mb-4">
@ -258,39 +293,7 @@
</div>
{% endif %}
<!-- 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 -->
{% if is_cancelled %}
<div class="row mb-4">
@ -327,7 +330,7 @@
class="btn btn-info ms-2">{% trans "View Full Estimate" %}</a>
{% endif %}
{% 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>
{% endif %}
</div>

View File

@ -66,7 +66,7 @@
<td class="align-middle white-space-nowrap invoice">
{% if tx.invoice and perms.django_ledger.view_invoicemodel %}
<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>
{% if tx.invoice.is_draft %}
<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>
</div>
<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 %}
{{ redirect_field }}
{{ form.name|as_crispy_field }}
@ -32,7 +32,7 @@
{{ form.email|as_crispy_field }}
{{ form.phone_number|as_crispy_field }}
{{ form.address|as_crispy_field }}
{{ form.image|as_crispy_field }}
{{ form.logo|as_crispy_field }}
{{ form.group|as_crispy_field }}
{% for error in form.errors %}<div class="text-danger">{{ error }}</div>{% endfor %}
<hr class="my-2">

View File

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