new chnages from ismail
@ -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
|
||||
])
|
||||
),
|
||||
})
|
||||
@ -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)),
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"),
|
||||
# }
|
||||
# ),
|
||||
}
|
||||
|
||||
|
||||
|
||||
79
inventory/management/commands/led.py
Normal 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()
|
||||
117
inventory/management/commands/seed.py
Normal 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,
|
||||
)
|
||||
242
inventory/management/commands/seed1.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -721,6 +721,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(
|
||||
@ -1177,7 +1183,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
|
||||
@ -1275,7 +1287,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",
|
||||
@ -1500,6 +1512,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(
|
||||
@ -1642,7 +1660,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"))
|
||||
@ -2456,7 +2480,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"))
|
||||
@ -2779,7 +2809,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
|
||||
@ -3304,7 +3334,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 []
|
||||
|
||||
@ -3313,12 +3343,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,
|
||||
@ -3337,7 +3373,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 []
|
||||
|
||||
@ -3346,17 +3382,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
|
||||
|
||||
0
inventory/notifications/__init__.py
Normal file
205
inventory/notifications/sse.py
Normal 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""
|
||||
})
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -371,14 +372,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}")
|
||||
|
||||
@ -960,24 +961,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)
|
||||
@ -1006,7 +1007,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:
|
||||
@ -1100,7 +1101,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"""
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
@ -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,
|
||||
|
||||
@ -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,9 +26,12 @@ 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
|
||||
logger=logging.getLogger(__name__)
|
||||
from django_ledger.io import roles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_random_password(
|
||||
length=10, allowed_chars="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
@ -453,30 +457,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):
|
||||
@ -997,12 +1006,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()
|
||||
@ -1010,77 +1032,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)
|
||||
@ -1089,11 +1096,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
|
||||
),
|
||||
@ -1113,9 +1120,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):
|
||||
@ -1172,135 +1337,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=journal,
|
||||
account=cash_account,
|
||||
amount=Decimal(finance_data.get("grand_total")),
|
||||
tx_type="debit",
|
||||
description="",
|
||||
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=revenue_account,
|
||||
amount=Decimal(finance_data.get("grand_total")),
|
||||
tx_type="credit",
|
||||
description="",
|
||||
journal_entry=je_sale,
|
||||
account=vat_acc,
|
||||
amount=vat_amount,
|
||||
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}",
|
||||
# Cr Sales – Car
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=car_rev,
|
||||
amount=net_car_price,
|
||||
tx_type='credit'
|
||||
)
|
||||
|
||||
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,
|
||||
origin="Payment",
|
||||
posted=False
|
||||
)
|
||||
|
||||
# Dr COGS
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=journal_cogs,
|
||||
account=cogs_account,
|
||||
amount=Decimal(car.finances.cost_price),
|
||||
tx_type="debit",
|
||||
description="",
|
||||
journal_entry=je_cogs,
|
||||
account=cogs_acc,
|
||||
amount=cost_total,
|
||||
tx_type='debit'
|
||||
)
|
||||
|
||||
# Cr Inventory
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=journal_cogs,
|
||||
account=inventory_account,
|
||||
amount=Decimal(car.finances.cost_price),
|
||||
tx_type="credit",
|
||||
description="",
|
||||
journal_entry=je_cogs,
|
||||
account=inv_acc,
|
||||
amount=cost_total,
|
||||
tx_type='credit'
|
||||
)
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
# 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,
|
||||
|
||||
@ -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()
|
||||
@ -1518,13 +1516,13 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
||||
print(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(
|
||||
@ -1571,25 +1569,22 @@ 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
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = self.object.car
|
||||
return context
|
||||
|
||||
class CarUpdateView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
||||
@ -2409,7 +2404,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():
|
||||
@ -4332,21 +4326,22 @@ 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)
|
||||
query = request.GET.get('q','').strip()
|
||||
if query:
|
||||
qs = qs.filter(
|
||||
Q(customer__first_name__icontains=query)|
|
||||
Q(customer__last_name__icontains=query)|
|
||||
Q(customer__address__icontains=query)|
|
||||
Q(customer__phone_number__icontains=query)|
|
||||
Q(estimate__estimate_number__icontains=query)|
|
||||
Q(invoice__invoice_number__icontains=query)).distinct()
|
||||
|
||||
|
||||
# query = request.GET.get('q')
|
||||
# # if query:
|
||||
# # qs = qs.filter(
|
||||
# # Q(order_number__icontains=query) |
|
||||
# # Q(customer__name__icontains=query) |
|
||||
# # Q(item_details__icontains=query)
|
||||
|
||||
# # ).distinct()
|
||||
# for so in qs:
|
||||
# if query in so.customer_customer
|
||||
|
||||
paginator = Paginator(qs, 30)
|
||||
page_number = request.GET.get("page")
|
||||
@ -4421,7 +4416,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(
|
||||
[
|
||||
@ -4434,16 +4428,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
|
||||
|
||||
@ -4585,6 +4582,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,
|
||||
@ -4647,11 +4645,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:
|
||||
@ -4845,21 +4843,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},
|
||||
)
|
||||
|
||||
@ -4878,6 +4876,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)
|
||||
|
||||
|
||||
@ -4894,7 +4893,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)
|
||||
|
||||
|
||||
@ -5153,9 +5152,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)
|
||||
|
||||
@ -5291,7 +5290,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},
|
||||
)
|
||||
|
||||
|
||||
@ -5339,7 +5338,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):
|
||||
@ -5347,7 +5346,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()
|
||||
@ -5379,12 +5378,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
|
||||
@ -5447,7 +5446,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(
|
||||
@ -5507,6 +5506,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)
|
||||
@ -5535,15 +5560,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"
|
||||
@ -5552,19 +5577,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:
|
||||
@ -5572,14 +5597,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 "
|
||||
@ -5600,7 +5625,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}
|
||||
)
|
||||
|
||||
|
||||
@ -5731,7 +5756,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
|
||||
@ -6583,41 +6608,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")
|
||||
@ -6649,25 +6676,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},
|
||||
@ -6751,14 +6783,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):
|
||||
@ -9310,7 +9346,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"
|
||||
@ -10264,6 +10300,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(
|
||||
|
||||
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/logos/staff/customer2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
static/images/logos/users/customer1.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
static/images/user-logo.png
Normal file
|
After Width: | Height: | Size: 814 B |
BIN
static/user-logo.png
Normal file
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
staticfiles/images/logos/staff/customer2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
staticfiles/images/logos/users/customer1.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
staticfiles/images/user-logo.png
Normal file
|
After Width: | Height: | Size: 814 B |
51
staticfiles/js/formSubmitHandler.js
Normal 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
|
After Width: | Height: | Size: 814 B |
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
16
templates/components/email_modal.html
Normal 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>
|
||||
@ -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 -->
|
||||
|
||||
@ -43,7 +43,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">
|
||||
|
||||
@ -5,36 +5,63 @@
|
||||
{{ _("Leads") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="card email-content">
|
||||
<h5 class="card-header">Send Mail</h5>
|
||||
<div class="card-body">
|
||||
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' request.dealer.slug lead.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-12">
|
||||
<input class="form-control" id="to" name="to" type="text" placeholder="To" value="{{lead.email}}" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input class="form-control" id="subject" name="subject" type="text" placeholder="Subject" value="{{subject}}" />
|
||||
</div>
|
||||
<div class="card email-content">
|
||||
<h5 class="card-header">Send Mail</h5>
|
||||
<div class="card-body">
|
||||
<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 %}
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-12">
|
||||
<input class="form-control"
|
||||
id="to"
|
||||
name="to"
|
||||
type="text"
|
||||
placeholder="To"
|
||||
value="{{ lead.email }}" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input class="form-control"
|
||||
id="subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value="{{ subject }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 flex-1">
|
||||
<textarea class="form-control h-70"
|
||||
id="message"
|
||||
name="message"
|
||||
rows="15"
|
||||
placeholder="Message">{{message}}</textarea>
|
||||
</div>
|
||||
<div class="mb-3 flex-1">
|
||||
<input class="form-control"
|
||||
id="email_pk"
|
||||
name="email_pk"
|
||||
type="text"
|
||||
hidden
|
||||
value="{{ email_pk }}" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
{% 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"
|
||||
href="{% url 'send_lead_email' request.dealer.slug lead.slug %}?status=draft"
|
||||
class="btn btn-phoenix-success">Save as Draft</a>
|
||||
<button class="btn btn-phoenix-primary fs-10" type="submit">
|
||||
Send<span class="fa-solid fa-paper-plane ms-1"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-3 flex-1">
|
||||
<textarea class="form-control h-70" id="message" name="message" rows="15" placeholder="Message">{{message}}</textarea>
|
||||
</div>
|
||||
<div class="mb-3 flex-1">
|
||||
<input class="form-control" id="email_pk" name="email_pk" type="text" hidden value="{{email_pk}}" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug%}" class="btn btn-phoenix-danger">{% trans 'Discard' %}</a>
|
||||
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' request.dealer.slug lead.slug %}?status=draft" class="btn btn-phoenix-success">Save as Draft</a>
|
||||
<button class="btn btn-phoenix-primary fs-10" type="submit">{% trans 'Send' %}<span class="fa-solid fa-paper-plane ms-1"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
{% 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' %}" />
|
||||
@ -203,4 +204,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<!---->
|
||||
{% endblock content %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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" %}
|
||||
|
||||
29
templates/sales/invoices/invoice_update.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
43
templates/sales/payments/payment_form1.html
Normal 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 %}
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
73
templates/sales/tags/invoice_item_formset.html
Normal 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>
|
||||
@ -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>
|
||||
|
||||