Merge remote-tracking branch 'origin/main'

This commit is contained in:
Marwan Alwali 2025-09-24 16:24:45 +03:00
commit 5cfe6d6093
163 changed files with 9203 additions and 7323 deletions

3
.gitignore vendored
View File

@ -163,8 +163,11 @@ GitHub.sublime-settings
.history .history
static-copy
static static
static/*
staticfiles staticfiles
media media
tmp tmp
logs logs
static/testdir

View File

@ -17,40 +17,24 @@ import django
django.setup() django.setup()
from django.urls import path from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from whitenoise import WhiteNoise
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from api import routing
from inventory.notifications.sse import NotificationSSEApp from inventory.notifications.sse import NotificationSSEApp
from django.urls import re_path from django.urls import re_path
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from pathlib import Path
# application = ProtocolTypeRouter(
# {
# "http": get_asgi_application(),
# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
# }
# )
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup()
BASE_DIR = Path(__file__).resolve().parent.parent
# BASE_DIR = Path(__file__).resolve().parent.parent
app = get_asgi_application() app = get_asgi_application()
# app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles'))
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": AuthMiddlewareStack( "http": AuthMiddlewareStack(
URLRouter( URLRouter(
[ [
path("sse/notifications/", NotificationSSEApp()), path("sse/notifications/", NotificationSSEApp()),
re_path( re_path(r"", app),
r"", app
), # All other routes go to Django
] ]
) )
), ),
@ -58,5 +42,5 @@ application = ProtocolTypeRouter(
) )
if django.conf.settings.DEBUG: # if django.conf.settings.DEBUG:
application = ASGIStaticFilesHandler(app) # application = ASGIStaticFilesHandler(app)

View File

@ -5,7 +5,7 @@ from django.urls import path, include
from schema_graph.views import Schema from schema_graph.views import Schema
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from inventory.notifications.sse import NotificationSSEApp # from inventory.notifications.sse import NotificationSSEApp
# import debug_toolbar # import debug_toolbar
# from two_factor.urls import urlpatterns as tf_urls # from two_factor.urls import urlpatterns as tf_urls
@ -33,5 +33,6 @@ urlpatterns += i18n_patterns(
# path('', include(tf_urls)), # path('', include(tf_urls)),
) )
# if not settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -3,6 +3,7 @@ from django.contrib import admin
from . import models from . import models
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
from django.contrib import messages from django.contrib import messages
# from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait # from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait
# from appointment import models as appointment_models # from appointment import models as appointment_models
from import_export.admin import ExportMixin from import_export.admin import ExportMixin
@ -177,55 +178,52 @@ class CarOptionAdmin(admin.ModelAdmin):
# actions = [export_to_pdf_landscape, export_to_pdf_portrait] # actions = [export_to_pdf_landscape, export_to_pdf_portrait]
@admin.register(models.UserRegistration) @admin.register(models.UserRegistration)
class UserRegistrationAdmin(admin.ModelAdmin): class UserRegistrationAdmin(admin.ModelAdmin):
# Fields to display in the list view # Fields to display in the list view
list_display = [ list_display = [
'name', "name",
'arabic_name', "arabic_name",
'email', "email",
'crn', "crn",
'vrn', "vrn",
'phone_number', "phone_number",
'is_created', "is_created",
'created_at', "created_at",
] ]
# Filters in the right sidebar # Filters in the right sidebar
list_filter = [ list_filter = [
'is_created', "is_created",
'created_at', "created_at",
] ]
# Searchable fields # Searchable fields
search_fields = [ search_fields = ["name", "arabic_name", "email", "crn", "vrn", "phone_number"]
'name', 'arabic_name', 'email', 'crn', 'vrn', 'phone_number'
]
# Read-only fields in detail view # Read-only fields in detail view
readonly_fields = [ readonly_fields = ["created_at", "updated_at", "is_created", "password"]
'created_at', 'updated_at', 'is_created', 'password'
]
# Organize form layout # Organize form layout
fieldsets = [ fieldsets = [
('Account Information', { (
'fields': ('name', 'arabic_name', 'email', 'phone_number') "Account Information",
}), {"fields": ("name", "arabic_name", "email", "phone_number")},
('Business Details', { ),
'fields': ('crn', 'vrn', 'address') ("Business Details", {"fields": ("crn", "vrn", "address")}),
}), (
('Status', { "Status",
'fields': ('is_created', 'password', 'created_at', 'updated_at'), {
'classes': ('collapse',) "fields": ("is_created", "password", "created_at", "updated_at"),
}), "classes": ("collapse",),
},
),
] ]
# Custom action to create accounts # Custom action to create accounts
actions = ['create_dealer_accounts'] actions = ["create_dealer_accounts"]
@admin.action(description='Create dealer account(s) for selected registrations') @admin.action(description="Create dealer account(s) for selected registrations")
def create_dealer_accounts(self, request, queryset): def create_dealer_accounts(self, request, queryset):
created_count = 0 created_count = 0
already_created_count = 0 already_created_count = 0
@ -242,7 +240,7 @@ class UserRegistrationAdmin(admin.ModelAdmin):
self.message_user( self.message_user(
request, request,
f"Error creating account for {registration.name}: {str(e)}", f"Error creating account for {registration.name}: {str(e)}",
level=messages.ERROR level=messages.ERROR,
) )
failed_count += 1 failed_count += 1
@ -251,17 +249,17 @@ class UserRegistrationAdmin(admin.ModelAdmin):
self.message_user( self.message_user(
request, request,
f"Successfully created {created_count} account(s).", f"Successfully created {created_count} account(s).",
level=messages.SUCCESS level=messages.SUCCESS,
) )
if already_created_count > 0: if already_created_count > 0:
self.message_user( self.message_user(
request, request,
f"{already_created_count} registration(s) were already created.", f"{already_created_count} registration(s) were already created.",
level=messages.INFO level=messages.INFO,
) )
if failed_count > 0: if failed_count > 0:
self.message_user( self.message_user(
request, request,
f"Failed to create {failed_count} account(s). Check logs.", f"Failed to create {failed_count} account(s). Check logs.",
level=messages.ERROR level=messages.ERROR,
) )

View File

@ -2,6 +2,7 @@ from django.core.cache import cache
from datetime import datetime from datetime import datetime
from luhnchecker.luhn import Luhn from luhnchecker.luhn import Luhn
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
# from appointment.models import Service # from appointment.models import Service
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django import forms from django import forms
@ -57,7 +58,7 @@ from .models import (
Tasks, Tasks,
Recall, Recall,
Ticket, Ticket,
UserRegistration UserRegistration,
) )
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
from django.forms import ( from django.forms import (
@ -364,7 +365,14 @@ class CarForm(
"receiving_date", "receiving_date",
"vendor", "vendor",
] ]
required_fields = ["vin","id_car_make", "id_car_model", "id_car_serie", "id_car_trim", "vendor"] required_fields = [
"vin",
"id_car_make",
"id_car_model",
"id_car_serie",
"id_car_trim",
"vendor",
]
widgets = { widgets = {
"id_car_make": forms.Select(attrs={"class": "form-select form-select-sm"}), "id_car_make": forms.Select(attrs={"class": "form-select form-select-sm"}),
"receiving_date": forms.DateTimeInput(attrs={"type": "datetime-local"}), "receiving_date": forms.DateTimeInput(attrs={"type": "datetime-local"}),
@ -2123,8 +2131,7 @@ class AdditionalFinancesForm(forms.Form):
for field in self.fields.values(): for field in self.fields.values():
if isinstance(field, forms.ModelMultipleChoiceField): if isinstance(field, forms.ModelMultipleChoiceField):
field.widget.choices = [ field.widget.choices = [
(obj.pk, f"{obj.name} - {obj.price:.2f}") (obj.pk, f"{obj.name} - {obj.price:.2f}") for obj in field.queryset
for obj in field.queryset
] ]
@ -2141,6 +2148,7 @@ class VatRateForm(forms.ModelForm):
model = VatRate model = VatRate
fields = ["rate"] fields = ["rate"]
class CustomSetPasswordForm(SetPasswordForm): class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField( new_password1 = forms.CharField(
label="New Password", label="New Password",
@ -2258,17 +2266,25 @@ class TicketResolutionForm(forms.ModelForm):
self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")] self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]
class CarDealershipRegistrationForm(forms.ModelForm): class CarDealershipRegistrationForm(forms.ModelForm):
# Add additional fields for the registration form # Add additional fields for the registration form
class Meta: class Meta:
model = UserRegistration model = UserRegistration
fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address") fields = (
"name",
"arabic_name",
"email",
"phone_number",
"crn",
"vrn",
"address",
)
class CarDetailsEstimateCreate(forms.Form): class CarDetailsEstimateCreate(forms.Form):
customer = forms.ModelChoiceField( customer = forms.ModelChoiceField(
queryset=Customer.objects.all(), queryset=Customer.objects.filter(active=True),
required=True, required=True,
label="Customer", label="Customer",
widget=forms.Select(attrs={"class": "form-control"}), widget=forms.Select(attrs={"class": "form-control"}),

View File

@ -18,8 +18,8 @@ def check_create_coa_accounts(task):
logger.warning("Account creation task failed, checking status...") logger.warning("Account creation task failed, checking status...")
try: try:
dealer_id = task.kwargs.get('dealer_id',None) dealer_id = task.kwargs.get("dealer_id", None)
coa_slug = task.kwargs.get('coa_slug', None) coa_slug = task.kwargs.get("coa_slug", None)
logger.info(f"Checking accounts for dealer {dealer_id}") logger.info(f"Checking accounts for dealer {dealer_id}")
logger.info(f"COA slug: {coa_slug}") logger.info(f"COA slug: {coa_slug}")
if not dealer_id: if not dealer_id:
@ -37,7 +37,9 @@ def check_create_coa_accounts(task):
try: try:
coa = entity.get_coa_model_qs().get(slug=coa_slug) coa = entity.get_coa_model_qs().get(slug=coa_slug)
except Exception as e: except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") logger.error(
f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}"
)
else: else:
coa = entity.get_default_coa() coa = entity.get_default_coa()
if not coa: if not coa:
@ -49,7 +51,11 @@ def check_create_coa_accounts(task):
missing_accounts = [] missing_accounts = []
for account_data in get_accounts_data(): for account_data in get_accounts_data():
if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists(): if (
not entity.get_all_accounts()
.filter(coa_model=coa, code=account_data["code"])
.exists()
):
missing_accounts.append(account_data) missing_accounts.append(account_data)
logger.info(f"Missing account: {account_data['code']}") logger.info(f"Missing account: {account_data['code']}")
@ -62,6 +68,8 @@ def check_create_coa_accounts(task):
except Exception as e: except Exception as e:
logger.error(f"Error in check_create_coa_accounts hook: {e}") logger.error(f"Error in check_create_coa_accounts hook: {e}")
# def check_create_coa_accounts(task): # def check_create_coa_accounts(task):
# logger.info("Checking if all accounts are created") # logger.info("Checking if all accounts are created")
# instance = task.kwargs["dealer"] # instance = task.kwargs["dealer"]

View File

@ -8,19 +8,23 @@ from django.core.management.base import BaseCommand
User = get_user_model() User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
help = "Deactivates expired user plans" help = "Deactivates expired user plans"
def handle(self, *args, **options): def handle(self, *args, **options):
users_without_plan = User.objects.filter( users_without_plan = User.objects.filter(
is_active=True, userplan=None, dealer__isnull=False, date_joined__lte=timezone.now()-timedelta(days=7) is_active=True,
userplan=None,
dealer__isnull=False,
date_joined__lte=timezone.now() - timedelta(days=7),
) )
count = users_without_plan.count() count = users_without_plan.count()
for user in users_without_plan: for user in users_without_plan:
user.is_active = False user.is_active = False
user.save() user.save()
subject = 'Your account has been deactivated' subject = "Your account has been deactivated"
message = """ message = """
Hello {},\n Hello {},\n
Your account has been deactivated, please contact us at {} if you have any questions. Your account has been deactivated, please contact us at {} if you have any questions.
@ -30,7 +34,7 @@ class Command(BaseCommand):
""".format(user.dealer.name, settings.DEFAULT_FROM_EMAIL) """.format(user.dealer.name, settings.DEFAULT_FROM_EMAIL)
from_email = settings.DEFAULT_FROM_EMAIL from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = user.email recipient_list = user.email
send_email(from_email, recipient_list,subject, message) send_email(from_email, recipient_list, subject, message)
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(

View File

@ -63,7 +63,7 @@ class Command(BaseCommand):
for plan in expired_plans: for plan in expired_plans:
# try: # try:
if dealer := getattr(plan.user,"dealer", None): if dealer := getattr(plan.user, "dealer", None):
dealer.user.is_active = False dealer.user.is_active = False
dealer.user.save() dealer.user.save()
for staff in dealer.get_staff(): for staff in dealer.get_staff():

View File

@ -7,16 +7,16 @@ from django_ledger.models import EstimateModel, BillModel, AccountModel, LedgerM
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
Permission.objects.get_or_create( # Permission.objects.get_or_create(
name="Can view crm", # name="Can view crm",
codename="can_view_crm", # codename="can_view_crm",
content_type=ContentType.objects.get_for_model(Lead), # content_type=ContentType.objects.get_for_model(Lead),
) # )
Permission.objects.get_or_create( # Permission.objects.get_or_create(
name="Can reassign lead", # name="Can reassign lead",
codename="can_reassign_lead", # codename="can_reassign_lead",
content_type=ContentType.objects.get_for_model(Lead), # content_type=ContentType.objects.get_for_model(Lead),
) # )
Permission.objects.get_or_create( Permission.objects.get_or_create(
name="Can view sales", name="Can view sales",
codename="can_view_sales", codename="can_view_sales",
@ -47,4 +47,3 @@ class Command(BaseCommand):
codename="can_approve_estimatemodel", codename="can_approve_estimatemodel",
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
) )

View File

@ -23,19 +23,19 @@ class Command(BaseCommand):
# Note: Deleting plans and quotas should cascade to related objects like PlanQuota and PlanPricing. # Note: Deleting plans and quotas should cascade to related objects like PlanQuota and PlanPricing.
self.stdout.write(self.style.SUCCESS("Data reset complete.")) self.stdout.write(self.style.SUCCESS("Data reset complete."))
else: else:
self.stdout.write(self.style.NOTICE("Creating or updating default plans and quotas...")) self.stdout.write(
self.style.NOTICE("Creating or updating default plans and quotas...")
)
# Create or get quotas # Create or get quotas
users_quota, created_u = Quota.objects.get_or_create( users_quota, created_u = Quota.objects.get_or_create(
codename="Users", codename="Users", defaults={"name": "Users", "unit": "number"}
defaults={"name": "Users", "unit": "number"}
) )
if created_u: if created_u:
self.stdout.write(self.style.SUCCESS('Created quota: "Users"')) self.stdout.write(self.style.SUCCESS('Created quota: "Users"'))
cars_quota, created_c = Quota.objects.get_or_create( cars_quota, created_c = Quota.objects.get_or_create(
codename="Cars", codename="Cars", defaults={"name": "Cars", "unit": "number"}
defaults={"name": "Cars", "unit": "number"}
) )
if created_c: if created_c:
self.stdout.write(self.style.SUCCESS('Created quota: "Cars"')) self.stdout.write(self.style.SUCCESS('Created quota: "Cars"'))
@ -43,90 +43,81 @@ class Command(BaseCommand):
# Create or get plans # Create or get plans
basic_plan, created_bp = Plan.objects.get_or_create( basic_plan, created_bp = Plan.objects.get_or_create(
name="Basic", name="Basic",
defaults={"description": "basic plan", "available": True, "visible": True} defaults={"description": "basic plan", "available": True, "visible": True},
) )
if created_bp: if created_bp:
self.stdout.write(self.style.SUCCESS('Created plan: "Basic"')) self.stdout.write(self.style.SUCCESS('Created plan: "Basic"'))
pro_plan, created_pp = Plan.objects.get_or_create( pro_plan, created_pp = Plan.objects.get_or_create(
name="Pro", name="Pro",
defaults={"description": "Pro plan", "available": True, "visible": True} defaults={"description": "Pro plan", "available": True, "visible": True},
) )
if created_pp: if created_pp:
self.stdout.write(self.style.SUCCESS('Created plan: "Pro"')) self.stdout.write(self.style.SUCCESS('Created plan: "Pro"'))
enterprise_plan, created_ep = Plan.objects.get_or_create( enterprise_plan, created_ep = Plan.objects.get_or_create(
name="Enterprise", name="Enterprise",
defaults={"description": "Enterprise plan", "available": True, "visible": True} defaults={
"description": "Enterprise plan",
"available": True,
"visible": True,
},
) )
if created_ep: if created_ep:
self.stdout.write(self.style.SUCCESS('Created plan: "Enterprise"')) self.stdout.write(self.style.SUCCESS('Created plan: "Enterprise"'))
# Assign quotas to plans using get_or_create to prevent duplicates # Assign quotas to plans using get_or_create to prevent duplicates
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=basic_plan, plan=basic_plan, quota=users_quota, defaults={"value": 10000000}
quota=users_quota,
defaults={"value": 10000000}
) )
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=basic_plan, plan=basic_plan, quota=cars_quota, defaults={"value": 10000000}
quota=cars_quota,
defaults={"value": 10000000}
) )
# Pro plan quotas # Pro plan quotas
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=pro_plan, plan=pro_plan, quota=users_quota, defaults={"value": 10000000}
quota=users_quota,
defaults={"value": 10000000}
) )
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=pro_plan, plan=pro_plan, quota=cars_quota, defaults={"value": 10000000}
quota=cars_quota,
defaults={"value": 10000000}
) )
# Enterprise plan quotas # Enterprise plan quotas
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=enterprise_plan, plan=enterprise_plan, quota=users_quota, defaults={"value": 10000000}
quota=users_quota,
defaults={"value": 10000000}
) )
PlanQuota.objects.get_or_create( PlanQuota.objects.get_or_create(
plan=enterprise_plan, plan=enterprise_plan, quota=cars_quota, defaults={"value": 10000000}
quota=cars_quota,
defaults={"value": 10000000}
) )
# Create or get pricing # Create or get pricing
basic_pricing, created_bp_p = Pricing.objects.get_or_create( basic_pricing, created_bp_p = Pricing.objects.get_or_create(
name="3 Months", name="3 Months", defaults={"period": 90}
defaults={"period": 90}
) )
pro_pricing, created_pp_p = Pricing.objects.get_or_create( pro_pricing, created_pp_p = Pricing.objects.get_or_create(
name="6 Months", name="6 Months", defaults={"period": 180}
defaults={"period": 180}
) )
enterprise_pricing, created_ep_p = Pricing.objects.get_or_create( enterprise_pricing, created_ep_p = Pricing.objects.get_or_create(
name="1 Year", name="1 Year", defaults={"period": 365}
defaults={"period": 365}
) )
# Assign pricing to plans # Assign pricing to plans
PlanPricing.objects.get_or_create( PlanPricing.objects.get_or_create(
plan=basic_plan, plan=basic_plan,
pricing=basic_pricing, pricing=basic_pricing,
defaults={"price": Decimal("2997.00")} defaults={"price": Decimal("2997.00")},
) )
PlanPricing.objects.get_or_create( PlanPricing.objects.get_or_create(
plan=pro_plan, plan=pro_plan, pricing=pro_pricing, defaults={"price": Decimal("5395.00")}
pricing=pro_pricing,
defaults={"price": Decimal("5395.00")}
) )
PlanPricing.objects.get_or_create( PlanPricing.objects.get_or_create(
plan=enterprise_plan, plan=enterprise_plan,
pricing=enterprise_pricing, pricing=enterprise_pricing,
defaults={"price": Decimal("9590.00")} defaults={"price": Decimal("9590.00")},
) )
self.stdout.write(self.style.SUCCESS("Subscription plans structure successfully created or updated.")) self.stdout.write(
self.style.SUCCESS(
"Subscription plans structure successfully created or updated."
)
)

View File

@ -3,12 +3,13 @@ from django.core.management.base import BaseCommand
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.conf import settings from django.conf import settings
class Command(BaseCommand): class Command(BaseCommand):
help = 'Update the default site domain' help = "Update the default site domain"
def handle(self, *args, **options): def handle(self, *args, **options):
site = Site.objects.get_current() site = Site.objects.get_current()
site.domain = settings.SITE_DOMAIN site.domain = settings.SITE_DOMAIN
site.name = settings.SITE_NAME site.name = settings.SITE_NAME
site.save() site.save()
self.stdout.write(self.style.SUCCESS(f'Site updated to: {site.domain}')) self.stdout.write(self.style.SUCCESS(f"Site updated to: {site.domain}"))

View File

@ -10,7 +10,7 @@ from django.urls import reverse
# from django.utils.text import slugify # from django.utils.text import slugify
from slugify import slugify from slugify import slugify
from django.utils import timezone from django.utils import timezone
from django.core.validators import MinValueValidator,MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
import hashlib import hashlib
from django.db import models from django.db import models
from datetime import timedelta from datetime import timedelta
@ -57,15 +57,17 @@ from encrypted_model_fields.fields import (
EncryptedEmailField, EncryptedEmailField,
EncryptedTextField, EncryptedTextField,
) )
# from plans.models import AbstractPlan # from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords # from simple_history.models import HistoricalRecords
from plans.models import Invoice from plans.models import Invoice
from django_extensions.db.fields import RandomCharField,AutoSlugField from django_extensions.db.fields import RandomCharField, AutoSlugField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
class Base(models.Model): class Base(models.Model):
id = models.UUIDField( id = models.UUIDField(
unique=True, unique=True,
@ -206,11 +208,8 @@ class VatRate(models.Model):
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=Decimal("0.15"), default=Decimal("0.15"),
validators=[ validators=[MinValueValidator(0.0), MaxValueValidator(1.0)],
MinValueValidator(0.0), help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"),
MaxValueValidator(1.0)
],
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)")
) )
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@ -776,7 +775,7 @@ class Car(Base):
make = self.id_car_make.name if self.id_car_make else "Unknown Make" make = self.id_car_make.name if self.id_car_make else "Unknown Make"
model = self.id_car_model.name if self.id_car_model else "Unknown Model" model = self.id_car_model.name if self.id_car_model else "Unknown Model"
trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
vin=self.vin if self.vin else None vin = self.vin if self.vin else None
return f"{self.year} - {make} - {model} - {trim}-{vin}" return f"{self.year} - {make} - {model} - {trim}-{vin}"
@property @property
@ -843,7 +842,7 @@ class Car(Base):
def mark_as_sold(self): def mark_as_sold(self):
self.cancel_reservation() self.cancel_reservation()
self.status = CarStatusChoices.SOLD self.status = CarStatusChoices.SOLD
self.sold_date=timezone.now() self.sold_date = timezone.now()
self.save() self.save()
def cancel_reservation(self): def cancel_reservation(self):
@ -904,16 +903,23 @@ class Car(Base):
def get_active_estimates(self): def get_active_estimates(self):
try: try:
qs = self.item_model.itemtransactionmodel_set.exclude(ce_model__status="canceled") qs = self.item_model.itemtransactionmodel_set.exclude(
ce_model__status="canceled"
)
data = [] data = []
for item in qs: for item in qs:
x = ExtraInfo.objects.filter(object_id=item.ce_model.pk,content_type=ContentType.objects.get_for_model(EstimateModel)).first() x = ExtraInfo.objects.filter(
object_id=item.ce_model.pk,
content_type=ContentType.objects.get_for_model(EstimateModel),
).first()
if x: if x:
data.append(x) data.append(x)
return data return data
except Exception as e: except Exception as e:
logger.error(f"Error getting active estimates for car {self.vin} error: {e}") logger.error(
f"Error getting active estimates for car {self.vin} error: {e}"
)
return [] return []
@property @property
@ -1389,7 +1395,11 @@ class Dealer(models.Model, LocalizedNameMixin):
options={"quality": 80}, options={"quality": 80},
) )
entity = models.ForeignKey( entity = models.ForeignKey(
EntityModel, on_delete=models.SET_NULL, null=True, blank=True,related_name="dealers" EntityModel,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="dealers",
) )
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -1421,6 +1431,7 @@ class Dealer(models.Model, LocalizedNameMixin):
except Exception as e: except Exception as e:
print(e) print(e)
return None return None
@property @property
def is_plan_expired(self): def is_plan_expired(self):
try: try:
@ -1455,6 +1466,7 @@ class Dealer(models.Model, LocalizedNameMixin):
def get_vendors(self): def get_vendors(self):
return VendorModel.objects.filter(entity_model=self.entity) return VendorModel.objects.filter(entity_model=self.entity)
def get_staff(self): def get_staff(self):
return Staff.objects.filter(dealer=self) return Staff.objects.filter(dealer=self)
@ -1505,7 +1517,9 @@ class Staff(models.Model):
first_name = models.CharField(max_length=255, verbose_name=_("First Name")) first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"),null=True,blank=True) arabic_name = models.CharField(
max_length=255, verbose_name=_("Arabic Name"), null=True, blank=True
)
phone_number = EncryptedCharField( phone_number = EncryptedCharField(
max_length=255, max_length=255,
verbose_name=_("Phone Number"), verbose_name=_("Phone Number"),
@ -1794,7 +1808,7 @@ class Customer(models.Model):
commit=False, commit=False,
customer_model_kwargs={ customer_model_kwargs={
"customer_name": self.full_name, "customer_name": self.full_name,
"address_1": "",#self.address, "address_1": "", # self.address,
# "phone": self.phone_number, # "phone": self.phone_number,
# "email": self.email, # "email": self.email,
}, },
@ -2120,6 +2134,10 @@ class Lead(models.Model):
slug = RandomCharField(length=8, unique=True) slug = RandomCharField(length=8, unique=True)
class Meta: class Meta:
permissions = [
("can_view_crm", _("Can view CRM")),
("can_reassign_lead", _("Can reassign lead")),
]
verbose_name = _("Lead") verbose_name = _("Lead")
verbose_name_plural = _("Leads") verbose_name_plural = _("Leads")
indexes = [ indexes = [
@ -2305,10 +2323,14 @@ class Schedule(models.Model):
help_text=_("What is the status of this schedule?"), help_text=_("What is the status of this schedule?"),
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
auto_now_add=True, verbose_name=_("Created Date"), help_text=_("When was this schedule created?") auto_now_add=True,
verbose_name=_("Created Date"),
help_text=_("When was this schedule created?"),
) )
updated_at = models.DateTimeField( updated_at = models.DateTimeField(
auto_now=True, verbose_name=_("Updated Date"), help_text=_("When was this schedule last updated?") auto_now=True,
verbose_name=_("Updated Date"),
help_text=_("When was this schedule last updated?"),
) )
def __str__(self): def __str__(self):
@ -2495,13 +2517,12 @@ class Opportunity(models.Model):
def __str__(self): def __str__(self):
try: try:
if self.customer: if self.customer:
return ( return f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
)
return f"Opportunity for {self.organization.name}" return f"Opportunity for {self.organization.name}"
except Exception: except Exception:
return f"Opportunity for car :{self.car}" return f"Opportunity for car :{self.car}"
class Notes(models.Model): class Notes(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@ -2765,7 +2786,6 @@ class Vendor(models.Model, LocalizedNameMixin):
), ),
] ]
def __str__(self): def __str__(self):
return self.name return self.name
@ -3155,7 +3175,7 @@ class CustomGroup(models.Model):
"notes", "notes",
"tasks", "tasks",
"activity", "activity",
"additionalservices" "additionalservices",
], ],
) )
self.set_permissions( self.set_permissions(
@ -3272,7 +3292,7 @@ class CustomGroup(models.Model):
"payment", "payment",
"vendor", "vendor",
"additionalservices", "additionalservices",
'customer' "customer",
], ],
other_perms=[ other_perms=[
"view_car", "view_car",
@ -3340,7 +3360,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Cash account to track cash transactions when an invoice is created."), help_text=_(
"Cash account to track cash transactions when an invoice is created."
),
verbose_name=_("Invoice Cash Account"), verbose_name=_("Invoice Cash Account"),
) )
invoice_prepaid_account = models.ForeignKey( invoice_prepaid_account = models.ForeignKey(
@ -3349,7 +3371,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Prepaid Revenue account to track prepaid revenue when an invoice is created."), help_text=_(
"Prepaid Revenue account to track prepaid revenue when an invoice is created."
),
verbose_name=_("Invoice Prepaid Account"), verbose_name=_("Invoice Prepaid Account"),
) )
invoice_unearned_account = models.ForeignKey( invoice_unearned_account = models.ForeignKey(
@ -3358,7 +3382,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Unearned Revenue account to track unearned revenue when an invoice is created."), help_text=_(
"Unearned Revenue account to track unearned revenue when an invoice is created."
),
verbose_name=_("Invoice Unearned Account"), verbose_name=_("Invoice Unearned Account"),
) )
invoice_tax_payable_account = models.ForeignKey( invoice_tax_payable_account = models.ForeignKey(
@ -3367,7 +3393,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Tax Payable account to track tax liabilities when an invoice is created."), help_text=_(
"Tax Payable account to track tax liabilities when an invoice is created."
),
verbose_name=_("Invoice Tax Payable Account"), verbose_name=_("Invoice Tax Payable Account"),
) )
invoice_vehicle_sale_account = models.ForeignKey( invoice_vehicle_sale_account = models.ForeignKey(
@ -3376,7 +3404,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Vehicle Sales account to track vehicle sales revenue when an invoice is created."), help_text=_(
"Vehicle Sales account to track vehicle sales revenue when an invoice is created."
),
verbose_name=_("Invoice Vehicle Sale Account"), verbose_name=_("Invoice Vehicle Sale Account"),
) )
invoice_additional_services_account = models.ForeignKey( invoice_additional_services_account = models.ForeignKey(
@ -3385,7 +3415,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Additional Services account to track additional services revenue when an invoice is created."), help_text=_(
"Additional Services account to track additional services revenue when an invoice is created."
),
verbose_name=_("Invoice Additional Services Account"), verbose_name=_("Invoice Additional Services Account"),
) )
invoice_cost_of_good_sold_account = models.ForeignKey( invoice_cost_of_good_sold_account = models.ForeignKey(
@ -3394,7 +3426,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Cost of Goods Sold account to track the cost of goods sold when an invoice is created."), help_text=_(
"Cost of Goods Sold account to track the cost of goods sold when an invoice is created."
),
verbose_name=_("Invoice Cost of Goods Sold Account"), verbose_name=_("Invoice Cost of Goods Sold Account"),
) )
@ -3404,7 +3438,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Inventory account to track the cost of goods sold when an invoice is created."), help_text=_(
"Inventory account to track the cost of goods sold when an invoice is created."
),
verbose_name=_("Invoice Inventory Account"), verbose_name=_("Invoice Inventory Account"),
) )
@ -3423,7 +3459,9 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Prepaid account to track prepaid expenses when a bill is created."), help_text=_(
"Prepaid account to track prepaid expenses when a bill is created."
),
verbose_name=_("Bill Prepaid Account"), verbose_name=_("Bill Prepaid Account"),
) )
bill_unearned_account = models.ForeignKey( bill_unearned_account = models.ForeignKey(
@ -3432,10 +3470,14 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
help_text=_("Unearned account to track unearned expenses when a bill is created."), help_text=_(
"Unearned account to track unearned expenses when a bill is created."
),
verbose_name=_("Bill Unearned Account"), verbose_name=_("Bill Unearned Account"),
) )
additional_info = models.JSONField(default=dict, null=True, blank=True, help_text=_("Additional information")) additional_info = models.JSONField(
default=dict, null=True, blank=True, help_text=_("Additional information")
)
def __str__(self): def __str__(self):
return f"Settings for {self.dealer}" return f"Settings for {self.dealer}"
@ -3790,7 +3832,10 @@ class Ticket(models.Model):
] ]
dealer = models.ForeignKey( dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="tickets", verbose_name=_("Dealer") Dealer,
on_delete=models.CASCADE,
related_name="tickets",
verbose_name=_("Dealer"),
) )
subject = models.CharField( subject = models.CharField(
max_length=200, verbose_name=_("Subject"), help_text=_("Short description") max_length=200, verbose_name=_("Subject"), help_text=_("Short description")
@ -3882,7 +3927,6 @@ class CarImage(models.Model):
) )
class UserRegistration(models.Model): class UserRegistration(models.Model):
name = models.CharField(_("Name"), max_length=255) name = models.CharField(_("Name"), max_length=255)
arabic_name = models.CharField(_("Arabic Name"), max_length=255) arabic_name = models.CharField(_("Arabic Name"), max_length=255)
@ -3892,15 +3936,24 @@ class UserRegistration(models.Model):
verbose_name=_("Phone Number"), verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()], validators=[SaudiPhoneNumberValidator()],
) )
crn = models.CharField(_("Commercial Registration Number"), max_length=10, unique=True) crn = models.CharField(
_("Commercial Registration Number"), max_length=10, unique=True
)
vrn = models.CharField(_("Vehicle Registration Number"), max_length=15, unique=True) vrn = models.CharField(_("Vehicle Registration Number"), max_length=15, unique=True)
address = models.TextField(_("Address")) address = models.TextField(_("Address"))
password = models.CharField(_("Password"), max_length=255,null=True,blank=True) password = models.CharField(_("Password"), max_length=255, null=True, blank=True)
is_created = models.BooleanField(default=False) is_created = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
REQUIRED_FIELDS = ["username", "arabic_name", "crn", "vrn", "address", "phone_number"] REQUIRED_FIELDS = [
"username",
"arabic_name",
"crn",
"vrn",
"address",
"phone_number",
]
def __str__(self): def __str__(self):
return self.email return self.email
@ -3924,7 +3977,7 @@ class UserRegistration(models.Model):
phone=self.phone_number, phone=self.phone_number,
crn=self.crn, crn=self.crn,
vrn=self.vrn, vrn=self.vrn,
address=self.address address=self.address,
) )
if dealer: if dealer:

View File

@ -1,82 +1,3 @@
# 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 json
import time import time
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser

View File

@ -411,6 +411,22 @@ class BasePurchaseOrderActionActionView(
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}" f"Error: {e}"
) )
except Exception as e:
print(
f"User {user_username} encountered an exception "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
logger.warning(
f"User {user_username} encountered an exception "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
messages.add_message(
request,
message=f"Failed to update PO {po_model.po_number}. {e}",
level=messages.ERROR,
)
return response return response
@ -1129,6 +1145,7 @@ class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, Creat
}, },
) )
class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView): class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView):
context_object_name = "coa_model" context_object_name = "coa_model"
slug_url_kwarg = "coa_slug" slug_url_kwarg = "coa_slug"

View File

@ -5,6 +5,7 @@ from django.urls import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
# from appointment.models import Service # from appointment.models import Service
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -21,7 +22,7 @@ from django_ledger.models import (
EstimateModel, EstimateModel,
BillModel, BillModel,
ChartOfAccountModel, ChartOfAccountModel,
CustomerModel CustomerModel,
) )
from . import models from . import models
from django.utils.timezone import now from django.utils.timezone import now
@ -136,7 +137,6 @@ def create_car_location(sender, instance, created, **kwargs):
print(f"Failed to create CarLocation for car {instance.vin}: {e}") print(f"Failed to create CarLocation for car {instance.vin}: {e}")
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs): def create_ledger_entity(sender, instance, created, **kwargs):
if not created: if not created:
@ -155,20 +155,22 @@ def create_ledger_entity(sender, instance, created, **kwargs):
raise Exception("Entity creation failed") raise Exception("Entity creation failed")
instance.entity = entity instance.entity = entity
instance.save(update_fields=['entity']) instance.save(update_fields=["entity"])
# Create default COA # Create default COA
entity.create_chart_of_accounts( entity.create_chart_of_accounts(
assign_as_default=True, assign_as_default=True, commit=True, coa_name=f"{entity.name}-COA"
commit=True,
coa_name=f"{entity.name}-COA"
) )
logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.") logger.info(
f"✅ Setup complete for dealer {instance.id}: entity & COA ready."
)
except Exception as e: except Exception as e:
logger.error(f"💥 Failed setup for dealer {instance.id}: {e}") logger.error(f"💥 Failed setup for dealer {instance.id}: {e}")
# Optional: schedule retry or alert # Optional: schedule retry or alert
# Create Entity # Create Entity
# @receiver(post_save, sender=models.Dealer) # @receiver(post_save, sender=models.Dealer)
# def create_ledger_entity(sender, instance, created, **kwargs): # def create_ledger_entity(sender, instance, created, **kwargs):
@ -218,10 +220,10 @@ def create_ledger_entity(sender, instance, created, **kwargs):
# dealer=instance, # dealer=instance,
# hook="inventory.hooks.check_create_coa_accounts", # hook="inventory.hooks.check_create_coa_accounts",
# ) # )
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20)) # async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk) # create_settings(instance.pk)
# create_accounts_for_make(instance.pk) # create_accounts_for_make(instance.pk)
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
@ -998,16 +1000,28 @@ def save_po(sender, instance, created, **kwargs):
instance.itemtransactionmodel_set.first().po_model.save() instance.itemtransactionmodel_set.first().po_model.save()
except Exception as e: except Exception as e:
pass pass
@receiver(post_save, sender=PurchaseOrderModel) @receiver(post_save, sender=PurchaseOrderModel)
def create_po_item_upload(sender, instance, created, **kwargs): def create_po_item_upload(sender, instance, created, **kwargs):
if instance.po_status == "fulfilled" or instance.po_status == 'approved': if instance.po_status == "fulfilled" or instance.po_status == "approved":
for item in instance.get_itemtxs_data()[0]: for item in instance.get_itemtxs_data()[0]:
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
if item.bill_model.is_paid(): if item.bill_model and item.bill_model.is_paid():
models.PoItemsUploaded.objects.get_or_create( models.PoItemsUploaded.objects.update_or_create(
dealer=dealer, po=instance, item=item, status=instance.po_status dealer=dealer,
po=instance,
item=item,
defaults={"status": instance.po_status},
) )
# po_item = models.PoItemsUploaded.objects.get_or_create(
# dealer=dealer, po=instance, item=item,
# defaults={
# "status":instance.po_status
# }
# )
# @receiver(post_save, sender=models.Staff) # @receiver(post_save, sender=models.Staff)
# def add_service_to_staff(sender, instance, created, **kwargs): # def add_service_to_staff(sender, instance, created, **kwargs):
@ -1354,7 +1368,9 @@ def handle_car_image(sender, instance, created, **kwargs):
# ) # )
# Check for existing image with same hash # Check for existing image with same hash
existing = os.path.exists(os.path.join(settings.MEDIA_ROOT, "car_images",car.get_hash + ".png")) existing = os.path.exists(
os.path.join(settings.MEDIA_ROOT, "car_images", car.get_hash + ".png")
)
# existing = ( # existing = (
# models.CarImage.objects.filter( # models.CarImage.objects.filter(
# image_hash=car.get_hash, image__isnull=False # image_hash=car.get_hash, image__isnull=False
@ -1396,7 +1412,7 @@ def handle_user_registration(sender, instance, created, **kwargs):
""" """
Thank you for registering with us. We will contact you shortly to complete your application. Thank you for registering with us. We will contact you shortly to complete your application.
شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك. شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك.
""" """,
) )
if instance.is_created: if instance.is_created:
@ -1420,7 +1436,8 @@ def handle_user_registration(sender, instance, created, **kwargs):
يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا. يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا.
شكرا لاختيارك لنا. شكرا لاختيارك لنا.
""") """,
)
@receiver(post_save, sender=ChartOfAccountModel) @receiver(post_save, sender=ChartOfAccountModel)

View File

@ -12,12 +12,14 @@ from django.db import transaction
from django_ledger.io import roles from django_ledger.io import roles
from django_q.tasks import async_task from django_q.tasks import async_task
from django.core.mail import send_mail from django.core.mail import send_mail
# from appointment.models import StaffMember # from appointment.models import StaffMember
from django.utils.translation import activate from django.utils.translation import activate
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
# from .utils import get_accounts_data, create_account # from .utils import get_accounts_data, create_account
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -30,7 +32,7 @@ from inventory.models import (
CarReservation, CarReservation,
CarStatusChoices, CarStatusChoices,
CarImage, CarImage,
Car Car,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -63,17 +65,18 @@ def create_settings(pk):
) )
def create_coa_accounts(dealer_id,**kwargs): def create_coa_accounts(dealer_id, **kwargs):
""" """
Idempotent: Creates only missing default accounts. Idempotent: Creates only missing default accounts.
Safe to retry. Returns True if all done. Safe to retry. Returns True if all done.
""" """
from .models import Dealer from .models import Dealer
from .utils import get_accounts_data, create_account from .utils import get_accounts_data, create_account
try: try:
dealer = Dealer.objects.get(pk=dealer_id) dealer = Dealer.objects.get(pk=dealer_id)
entity = dealer.entity entity = dealer.entity
coa_slug = kwargs.get('coa_slug', None) coa_slug = kwargs.get("coa_slug", None)
if not entity: if not entity:
logger.error(f"❌ No entity for dealer {dealer_id}") logger.error(f"❌ No entity for dealer {dealer_id}")
return False return False
@ -82,7 +85,9 @@ def create_coa_accounts(dealer_id,**kwargs):
try: try:
coa = entity.get_coa_model_qs().get(slug=coa_slug) coa = entity.get_coa_model_qs().get(slug=coa_slug)
except Exception as e: except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") logger.error(
f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}"
)
return False return False
else: else:
coa = entity.get_default_coa() coa = entity.get_default_coa()
@ -92,10 +97,13 @@ def create_coa_accounts(dealer_id,**kwargs):
return False return False
# Get missing accounts # Get missing accounts
existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True)) existing_codes = set(
entity.get_all_accounts()
.filter(coa_model=coa)
.values_list("code", flat=True)
)
accounts_to_create = [ accounts_to_create = [
acc for acc in get_accounts_data() acc for acc in get_accounts_data() if acc["code"] not in existing_codes
if acc["code"] not in existing_codes
] ]
if not accounts_to_create: if not accounts_to_create:
@ -122,6 +130,7 @@ def create_coa_accounts(dealer_id,**kwargs):
logger.error(f"💥 Task failed for dealer {dealer_id}: {e}") logger.error(f"💥 Task failed for dealer {dealer_id}: {e}")
raise # Let Django-Q handle retry if configured raise # Let Django-Q handle retry if configured
def retry_entity_creation(dealer_id, retry_count=0): def retry_entity_creation(dealer_id, retry_count=0):
""" """
Retry entity creation if initial attempt failed Retry entity creation if initial attempt failed
@ -164,8 +173,10 @@ def retry_entity_creation(dealer_id, retry_count=0):
async_task( async_task(
"inventory.tasks.retry_entity_creation", "inventory.tasks.retry_entity_creation",
dealer_id=dealer_id, dealer_id=dealer_id,
retry_count=retry_count + 1 retry_count=retry_count + 1,
) )
# def create_coa_accounts(**kwargs): # def create_coa_accounts(**kwargs):
# logger.info("creating all accounts are created") # logger.info("creating all accounts are created")
# instance = kwargs.get("dealer") # instance = kwargs.get("dealer")

View File

@ -13,6 +13,7 @@ from django.db.models import Case, Value, When, IntegerField
register = template.Library() register = template.Library()
@register.filter @register.filter
def is_negative(value): def is_negative(value):
""" """
@ -23,6 +24,7 @@ def is_negative(value):
except (ValueError, TypeError): except (ValueError, TypeError):
return False return False
@register.filter @register.filter
def get_percentage(value, total): def get_percentage(value, total):
try: try:
@ -501,8 +503,16 @@ def bill_item_formset_table(context, item_formset):
for item in item_formset: for item in item_formset:
if item: if item:
print(item.fields["item_model"]) print(item.fields["item_model"])
item.initial["quantity"] = item.instance.po_quantity if item.instance.po_quantity else item.instance.quantity item.initial["quantity"] = (
item.initial["unit_cost"] = item.instance.po_unit_cost if item.instance.po_unit_cost else item.instance.unit_cost item.instance.po_quantity
if item.instance.po_quantity
else item.instance.quantity
)
item.initial["unit_cost"] = (
item.instance.po_unit_cost
if item.instance.po_unit_cost
else item.instance.unit_cost
)
# print(item.instance.po_quantity) # print(item.instance.po_quantity)
# print(item.instance.po_unit_cost) # print(item.instance.po_unit_cost)
# print(item.instance.po_total_amount) # print(item.instance.po_total_amount)

View File

@ -2,7 +2,7 @@ from inventory.utils import get_user_type
from . import views from . import views
from django.urls import path from django.urls import path
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic import RedirectView,TemplateView from django.views.generic import RedirectView, TemplateView
from django_tables2.export.export import TableExport from django_tables2.export.export import TableExport
from django.conf.urls import handler403, handler400, handler404, handler500 from django.conf.urls import handler403, handler400, handler404, handler500
@ -10,14 +10,17 @@ urlpatterns = [
# main URLs # main URLs
path("", views.WelcomeView, name="welcome"), path("", views.WelcomeView, name="welcome"),
# path("signup/", views.dealer_signup, name="account_signup"), # path("signup/", views.dealer_signup, name="account_signup"),
path('signup/', views.CarDealershipSignUpView.as_view(), name='account_signup'), path("signup/", views.CarDealershipSignUpView.as_view(), name="account_signup"),
path('success/', TemplateView.as_view(template_name='account/success.html'), name='registration_success'), path(
"success/",
TemplateView.as_view(template_name="account/success.html"),
name="registration_success",
),
path("", views.HomeView, name="home"), path("", views.HomeView, name="home"),
# path('refund-policy/',views.refund_policy,name='refund_policy'), # path('refund-policy/',views.refund_policy,name='refund_policy'),
path("<slug:dealer_slug>/", views.HomeView, name="home"), path("<slug:dealer_slug>/", views.HomeView, name="home"),
# Tasks # Tasks
path("legal/", views.terms_and_privacy, name="terms_and_privacy"), path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'), # path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards # Dashboards
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"), # path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
@ -44,13 +47,18 @@ urlpatterns = [
views.assign_car_makes, views.assign_car_makes,
name="assign_car_makes", name="assign_car_makes",
), ),
# dashboards for manager, dealer, inventory and accounatant
path(
#dashboards for manager, dealer, inventory and accounatant "dashboards/<slug:dealer_slug>/general/",
path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"), views.general_dashboard,
#dashboard for sales name="general_dashboard",
path("dashboards/<slug:dealer_slug>/sales/", views.sales_dashboard, name="sales_dashboard"), ),
# dashboard for sales
path(
"dashboards/<slug:dealer_slug>/sales/",
views.sales_dashboard,
name="sales_dashboard",
),
path( path(
"<slug:dealer_slug>/cars/aging-inventory/list", "<slug:dealer_slug>/cars/aging-inventory/list",
views.aging_inventory_list_view, views.aging_inventory_list_view,
@ -786,7 +794,11 @@ urlpatterns = [
views.EstimateDetailView.as_view(), views.EstimateDetailView.as_view(),
name="estimate_detail", name="estimate_detail",
), ),
path('<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/', views.EstimatePrintView.as_view(), name='estimate_print'), path(
"<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/",
views.EstimatePrintView.as_view(),
name="estimate_print",
),
path( path(
"<slug:dealer_slug>/sales/estimates/create/", "<slug:dealer_slug>/sales/estimates/create/",
views.create_estimate, views.create_estimate,
@ -794,7 +806,7 @@ urlpatterns = [
), ),
path( path(
"<slug:dealer_slug>/sales/estimates/create/<slug:slug>/", "<slug:dealer_slug>/sales/estimates/create/<slug:slug>/",
views.create_estimate, views.estimate_create_from_opportunity,
name="estimate_create_from_opportunity", name="estimate_create_from_opportunity",
), ),
path( path(
@ -943,7 +955,6 @@ urlpatterns = [
views.ItemServiceUpdateView.as_view(), views.ItemServiceUpdateView.as_view(),
name="item_service_update", name="item_service_update",
), ),
path( path(
"<slug:dealer_slug>/items/services/<int:pk>/detail/", "<slug:dealer_slug>/items/services/<int:pk>/detail/",
views.ItemServiceDetailView.as_view(), views.ItemServiceDetailView.as_view(),
@ -1112,32 +1123,47 @@ urlpatterns = [
name="entity-ic-date", name="entity-ic-date",
), ),
# Chart of Accounts... # Chart of Accounts...
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/', path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/",
views.ChartOfAccountModelListView.as_view(), views.ChartOfAccountModelListView.as_view(),
name='coa-list'), name="coa-list",
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/', ),
path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/",
views.ChartOfAccountModelListView.as_view(inactive=True), views.ChartOfAccountModelListView.as_view(inactive=True),
name='coa-list-inactive'), name="coa-list-inactive",
path('<slug:dealer_slug>/<slug:entity_slug>/create/', ),
path(
"<slug:dealer_slug>/<slug:entity_slug>/create/",
views.ChartOfAccountModelCreateView.as_view(), views.ChartOfAccountModelCreateView.as_view(),
name='coa-create'), name="coa-create",
path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/', ),
path(
"<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/",
views.ChartOfAccountModelListView.as_view(), views.ChartOfAccountModelListView.as_view(),
name='coa-detail'), name="coa-detail",
path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/', ),
path(
"<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/",
views.ChartOfAccountModelUpdateView.as_view(), views.ChartOfAccountModelUpdateView.as_view(),
name='coa-update'), name="coa-update",
),
# ACTIONS.... # ACTIONS....
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/', path(
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'), "<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/",
name='coa-action-mark-as-default'), views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/', name="coa-action-mark-as-default",
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'), ),
name='coa-action-mark-as-active'), path(
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/', "<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/",
views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'), views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"),
name='coa-action-mark-as-inactive'), name="coa-action-mark-as-active",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"),
name="coa-action-mark-as-inactive",
),
# CASH FLOW STATEMENTS... # CASH FLOW STATEMENTS...
# Entities... # Entities...
path( path(
@ -1313,42 +1339,80 @@ urlpatterns = [
views.PurchaseOrderMarkAsVoidView.as_view(), views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void", name="po-action-mark-as-void",
), ),
# reports # reports
path( path(
"<slug:dealer_slug>/purchase-report/", "<slug:dealer_slug>/purchase-report/",
views.purchase_report_view, views.purchase_report_view,
name="po-report", name="po-report",
), ),
path('purchase-report/<slug:dealer_slug>/csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'), path(
"purchase-report/<slug:dealer_slug>/csv/",
views.purchase_report_csv_export,
name="purchase-report-csv-export",
),
path( path(
"<slug:dealer_slug>/car-sale-report/", "<slug:dealer_slug>/car-sale-report/",
views.car_sale_report_view, views.car_sale_report_view,
name="car-sale-report", name="car-sale-report",
), ),
path('<slug:dealer_slug>/car-sale-report/get_filtered_choices/',views.get_filtered_choices,name='get_filtered_choices'), path(
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'), "<slug:dealer_slug>/car-sale-report/get_filtered_choices/",
views.get_filtered_choices,
path('feature/recall/', views.RecallListView.as_view(), name='recall_list'), name="get_filtered_choices",
path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'), ),
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'), path(
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'), "car-sale-report/<slug:dealer_slug>/csv/",
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'), views.car_sale_report_csv_export,
name="car-sale-report-csv-export",
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), ),
path("feature/recall/", views.RecallListView.as_view(), name="recall_list"),
path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"),
path(
"feature/recall/<int:pk>/view/",
views.RecallDetailView.as_view(),
name="recall_detail",
),
path(
"feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create"
),
path(
"feature/recall/success/",
views.RecallSuccessView.as_view(),
name="recall_success",
),
path(
"<slug:dealer_slug>/schedules/calendar/",
views.schedule_calendar,
name="schedule_calendar",
),
# staff profile # staff profile
path('<slug:dealer_slug>/staff/<slug:slug>detail/', views.StaffDetailView.as_view(), name='staff_detail'), path(
"<slug:dealer_slug>/staff/<slug:slug>detail/",
views.StaffDetailView.as_view(),
name="staff_detail",
),
# tickets # tickets
path('help_center/view/', views.help_center, name='help_center'), path("help_center/view/", views.help_center, name="help_center"),
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'), path(
path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'), "<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list"
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'), ),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'), path(
"help_center/tickets/<slug:dealer_slug>/create/",
views.create_ticket,
name="create_ticket",
),
path(
"<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/",
views.ticket_detail,
name="ticket_detail",
),
path(
"help_center/tickets/<int:ticket_id>/update/",
views.ticket_update,
name="ticket_update",
),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), # path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
path('payment_results/', views.payment_result, name='payment_result'), path("payment_results/", views.payment_result, name="payment_result"),
] ]
handler404 = "inventory.views.custom_page_not_found_view" handler404 = "inventory.views.custom_page_not_found_view"

View File

@ -27,7 +27,7 @@ from django_ledger.models import (
VendorModel, VendorModel,
AccountModel, AccountModel,
EntityModel, EntityModel,
ChartOfAccountModel ChartOfAccountModel,
) )
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django_ledger.models.items import ItemModel from django_ledger.models.items import ItemModel
@ -1325,11 +1325,13 @@ def get_finance_data(estimate, dealer):
) )
discount = extra_info.data.get("discount", 0) discount = extra_info.data.get("discount", 0)
discount = Decimal(discount) discount = Decimal(discount)
additional_services = car.get_additional_services() additional_services = car.get_additional_services()
discounted_price = Decimal(car.marked_price) - discount discounted_price = Decimal(car.marked_price) - discount
vat_amount = discounted_price * vat.rate vat_amount = discounted_price * vat.rate
total_services_amount = additional_services.get("total")
total_services_vat = sum([x[1] for x in additional_services.get("services")]) total_services_vat = sum([x[1] for x in additional_services.get("services")])
total_services_amount_ = additional_services.get("total_")
total_vat = vat_amount + total_services_vat total_vat = vat_amount + total_services_vat
return { return {
"car": car, "car": car,
@ -1341,6 +1343,8 @@ def get_finance_data(estimate, dealer):
"additional_services": additional_services, "additional_services": additional_services,
"final_price": discounted_price + vat_amount, "final_price": discounted_price + vat_amount,
"total_services_vat": total_services_vat, "total_services_vat": total_services_vat,
"total_services_amount": total_services_amount,
"total_services_amount_": total_services_amount_,
"total_vat": total_vat, "total_vat": total_vat,
"grand_total": discounted_price + total_vat + additional_services.get("total"), "grand_total": discounted_price + total_vat + additional_services.get("total"),
} }
@ -1596,24 +1600,38 @@ def _post_sale_and_cogs(invoice, dealer):
1) Cash / A-R / VAT / Revenue journal 1) Cash / A-R / VAT / Revenue journal
2) COGS / Inventory journal 2) COGS / Inventory journal
""" """
entity:EntityModel = invoice.ledger.entity entity: EntityModel = invoice.ledger.entity
# calc = CarFinanceCalculator(invoice) # calc = CarFinanceCalculator(invoice)
data = get_finance_data(invoice, dealer) data = get_finance_data(invoice, dealer)
car = data.get("car") car = data.get("car")
coa:ChartOfAccountModel = entity.get_default_coa() coa: ChartOfAccountModel = entity.get_default_coa()
cash_acc = invoice.cash_account or dealer.settings.invoice_cash_account cash_acc = invoice.cash_account or dealer.settings.invoice_cash_account
vat_acc = dealer.settings.invoice_tax_payable_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() vat_acc = (
dealer.settings.invoice_tax_payable_account
or entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE)
.first()
)
car_rev = dealer.settings.invoice_vehicle_sale_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() car_rev = (
dealer.settings.invoice_vehicle_sale_account
or entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.INCOME_OPERATIONAL)
.first()
)
add_rev = dealer.settings.invoice_additional_services_account add_rev = dealer.settings.invoice_additional_services_account
if not add_rev: if not add_rev:
try: try:
add_rev = entity.get_default_coa_accounts().filter(name="After-Sales Services", active=True).first() add_rev = (
entity.get_default_coa_accounts()
.filter(name="After-Sales Services", active=True)
.first()
)
if not add_rev: if not add_rev:
add_rev = coa.create_account( add_rev = coa.create_account(
code="4020", code="4020",
@ -1623,16 +1641,28 @@ def _post_sale_and_cogs(invoice, dealer):
active=True, active=True,
) )
add_rev.role_default = False add_rev.role_default = False
add_rev.save(update_fields=['role_default']) add_rev.save(update_fields=["role_default"])
dealer.settings.invoice_additional_services_account = add_rev dealer.settings.invoice_additional_services_account = add_rev
dealer.settings.save() dealer.settings.save()
except Exception as e: except Exception as e:
logger.error(f"error find or create additional services account {e}") logger.error(f"error find or create additional services account {e}")
if car.get_additional_services_amount > 0 and not add_rev: if car.get_additional_services_amount > 0 and not add_rev:
raise Exception("additional services exist but not account found,please create account for the additional services and set as default in the settings") raise Exception(
cogs_acc = dealer.settings.invoice_cost_of_good_sold_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() "additional services exist but not account found,please create account for the additional services and set as default in the settings"
)
cogs_acc = (
dealer.settings.invoice_cost_of_good_sold_account
or entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.COGS)
.first()
)
inv_acc = dealer.settings.invoice_inventory_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() inv_acc = (
dealer.settings.invoice_inventory_account
or entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.ASSET_CA_INVENTORY)
.first()
)
net_car_price = Decimal(data["discounted_price"]) net_car_price = Decimal(data["discounted_price"])
net_additionals_price = Decimal(data["additional_services"]["total"]) net_additionals_price = Decimal(data["additional_services"]["total"])
@ -1687,11 +1717,12 @@ def _post_sale_and_cogs(invoice, dealer):
# tx_type='credit' # tx_type='credit'
# ) # )
if car.get_additional_services_amount > 0: if car.get_additional_services_amount > 0:
# Cr Sales Additional Services # Cr Sales Additional Services
if not add_rev: if not add_rev:
logger.warning(f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry.") logger.warning(
f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry."
)
else: else:
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=je_sale, journal_entry=je_sale,
@ -1929,7 +1960,7 @@ def handle_payment(request, dealer):
} }
# Get selected plan from session # Get selected plan from session
selected_plan_id = request.session.get('pending_plan_id') selected_plan_id = request.session.get("pending_plan_id")
if not selected_plan_id: if not selected_plan_id:
raise ValueError("No pending plan found in session") raise ValueError("No pending plan found in session")
from plans.models import PlanPricing from plans.models import PlanPricing
@ -1947,7 +1978,8 @@ def handle_payment(request, dealer):
"dealer_slug": dealer.slug, "dealer_slug": dealer.slug,
} }
payload = json.dumps({ payload = json.dumps(
{
"amount": total, "amount": total,
"currency": "SAR", "currency": "SAR",
"description": f"Payment for plan {pp.plan.name}", "description": f"Payment for plan {pp.plan.name}",
@ -1965,7 +1997,8 @@ def handle_payment(request, dealer):
"save_card": False, "save_card": False,
}, },
"metadata": metadata, "metadata": metadata,
}) }
)
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "") auth = (settings.MOYASAR_SECRET_KEY, "")
@ -1989,7 +2022,9 @@ def handle_payment(request, dealer):
gateway_response=data, gateway_response=data,
) )
logger.info(f"Payment initiated: {data}") logger.info(f"Payment initiated: {data}")
return data["source"]["transaction_url"],None return data["source"]["transaction_url"], None
# def handle_payment(request, order): # def handle_payment(request, order):
# logger.info(f"Handling payment for order {order}") # logger.info(f"Handling payment for order {order}")
# url = "https://api.moyasar.com/v1/payments" # url = "https://api.moyasar.com/v1/payments"
@ -2509,10 +2544,9 @@ def create_account(entity, coa, account_data):
logger.info(f"Created account: {account}") logger.info(f"Created account: {account}")
if account: if account:
account.role_default = account_data.get("default", False) account.role_default = account_data.get("default", False)
account.save(update_fields=['role_default']) account.save(update_fields=["role_default"])
return True return True
except IntegrityError: except IntegrityError:
return True # Already created by race condition return True # Already created by race condition
except Exception as e: except Exception as e:
@ -2520,6 +2554,7 @@ def create_account(entity, coa, account_data):
return False return False
# def create_account(entity, coa, account_data): # def create_account(entity, coa, account_data):
# try: # try:
# account = entity.create_account( # account = entity.create_account(
@ -2801,7 +2836,9 @@ def generate_car_image_simple(car):
# Save the resized image # Save the resized image
logger.info(f" {car.vin}") logger.info(f" {car.vin}")
with open( with open(
os.path.join(settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"), os.path.join(
settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"
),
"wb", "wb",
) as f: ) as f:
f.write(resized_data) f.write(resized_data)
@ -2817,9 +2854,7 @@ def generate_car_image_simple(car):
return {"success": False, "error": error_msg} return {"success": False, "error": error_msg}
def create_estimate_(dealer, car, customer):
def create_estimate_(dealer,car,customer):
entity = dealer.entity entity = dealer.entity
title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}" title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}"
estimate = entity.create_estimate( estimate = entity.create_estimate(

View File

@ -16,9 +16,10 @@ class SaudiPhoneNumberValidator(RegexValidator):
cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value)) cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value))
super().__call__(cleaned_value) super().__call__(cleaned_value)
def vat_rate_validator(value): def vat_rate_validator(value):
if value < 0 or value > 1: if value < 0 or value > 1:
raise ValidationError( raise ValidationError(
_('%(value)s is not a valid VAT rate. It must be between 0 and 1.'), _("%(value)s is not a valid VAT rate. It must be between 0 and 1."),
params={'value': value}, params={"value": value},
) )

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,9 @@ autobahn==24.4.2
Automat==25.4.16 Automat==25.4.16
Babel==2.15.0 Babel==2.15.0
beautifulsoup4==4.13.4 beautifulsoup4==4.13.4
blacknoise==1.2.0
blessed==1.21.0 blessed==1.21.0
Brotli==1.1.0
cattrs==25.1.1 cattrs==25.1.1
certifi==2025.7.9 certifi==2025.7.9
cffi==1.17.1 cffi==1.17.1
@ -19,15 +21,13 @@ constantly==23.10.4
crispy-bootstrap5==2025.6 crispy-bootstrap5==2025.6
cryptography==45.0.5 cryptography==45.0.5
cssbeautifier==1.15.4 cssbeautifier==1.15.4
daphne==4.2.1 cssselect2==0.8.0
defusedxml==0.7.1 defusedxml==0.7.1
diff-match-patch==20241021 diff-match-patch==20241021
distro==1.9.0 distro==1.9.0
Django==5.2.4 Django==5.2.4
django-allauth==65.10.0 django-allauth==65.10.0
django-appconf==1.1.0 django-appconf==1.1.0
django-appointment==3.8.0
django-background-tasks==1.2.8
django-bootstrap5==25.1 django-bootstrap5==25.1
django-ckeditor==6.7.3 django-ckeditor==6.7.3
django-cors-headers==4.7.0 django-cors-headers==4.7.0
@ -35,6 +35,7 @@ django-countries==7.6.1
django-crispy-forms==2.4 django-crispy-forms==2.4
django-debug-toolbar==5.2.0 django-debug-toolbar==5.2.0
django-easy-audit==1.3.7 django-easy-audit==1.3.7
django-encrypted-model-fields==0.6.5
django-extensions==4.1 django-extensions==4.1
django-filter==25.1 django-filter==25.1
django-imagekit==5.0.0 django-imagekit==5.0.0
@ -47,7 +48,6 @@ django-ordered-model==3.7.4
django-phonenumber-field==8.0.0 django-phonenumber-field==8.0.0
django-picklefield==3.3 django-picklefield==3.3
django-plans==2.0.0 django-plans==2.0.0
django-prometheus==2.4.1
django-q2==1.8.0 django-q2==1.8.0
django-query-builder==3.2.0 django-query-builder==3.2.0
django-schema-graph==3.1.0 django-schema-graph==3.1.0
@ -56,8 +56,6 @@ django-tables2==2.7.5
django-treebeard==4.7.1 django-treebeard==4.7.1
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
djangorestframework==3.16.0 djangorestframework==3.16.0
djhtml==3.0.8
djlint==1.36.4
dnspython==2.7.0 dnspython==2.7.0
docopt==0.6.2 docopt==0.6.2
EditorConfig==0.17.1 EditorConfig==0.17.1
@ -78,8 +76,6 @@ hyperlink==21.0.0
icalendar==6.3.1 icalendar==6.3.1
idna==3.10 idna==3.10
incremental==24.7.2 incremental==24.7.2
iron-core==1.2.1
iron-mq==0.9
jiter==0.10.0 jiter==0.10.0
jsbeautifier==1.15.4 jsbeautifier==1.15.4
json5==0.12.0 json5==0.12.0
@ -109,16 +105,17 @@ phonenumbers==8.13.42
pilkit==3.0 pilkit==3.0
pillow==10.4.0 pillow==10.4.0
priority==1.3.0 priority==1.3.0
prometheus_client==0.22.1
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
pyasn1==0.6.1 pyasn1==0.6.1
pyasn1_modules==0.4.2 pyasn1_modules==0.4.2
pycparser==2.22 pycparser==2.22
pydantic==2.11.7 pydantic==2.11.7
pydantic_core==2.33.2 pydantic_core==2.33.2
pydyf==0.11.0
Pygments==2.19.2 Pygments==2.19.2
pymongo==4.14.1 pymongo==4.14.1
pyOpenSSL==25.1.0 pyOpenSSL==25.1.0
pyphen==0.17.2
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.1.1 python-dotenv==1.1.1
python-slugify==8.0.4 python-slugify==8.0.4
@ -131,8 +128,6 @@ redis==6.2.0
regex==2024.11.6 regex==2024.11.6
requests==2.32.4 requests==2.32.4
requests-toolbelt==1.0.0 requests-toolbelt==1.0.0
rich==14.0.0
ruff==0.12.2
service-identity==24.2.0 service-identity==24.2.0
setuptools==80.9.0 setuptools==80.9.0
six==1.17.0 six==1.17.0
@ -140,11 +135,15 @@ sniffio==1.3.1
soupsieve==2.7 soupsieve==2.7
SQLAlchemy==2.0.41 SQLAlchemy==2.0.41
sqlparse==0.5.3 sqlparse==0.5.3
starlette==0.47.3
static3==0.7.0
suds==1.2.0 suds==1.2.0
swapper==1.3.0 swapper==1.3.0
tablib==3.8.0 tablib==3.8.0
tenacity==9.1.2 tenacity==9.1.2
text-unidecode==1.3 text-unidecode==1.3
tinycss2==1.4.0
tinyhtml5==2.0.0
tqdm==4.67.1 tqdm==4.67.1
Twisted==25.5.0 Twisted==25.5.0
txaio==25.6.1 txaio==25.6.1
@ -156,6 +155,8 @@ urllib3==2.5.0
uvicorn==0.35.0 uvicorn==0.35.0
uvicorn-worker==0.3.0 uvicorn-worker==0.3.0
wcwidth==0.2.13 wcwidth==0.2.13
whitenoise==6.9.0 weasyprint==66.0
webencodings==0.5.1
zope.interface==7.2 zope.interface==7.2
zopfli==0.2.3.post1
zstandard==0.23.0 zstandard==0.23.0

View File

@ -7,8 +7,8 @@
{% block content %} {% block content %}
{% element h1 %} {% element h1 %}
{% translate "Account Inactive" %} {% translate "Account Inactive" %}
{% endelement %} {% endelement %}
{% element p %} {% element p %}
{% translate "This account is inactive." %} {% translate "This account is inactive." %}
{% endelement %} {% endelement %}
{% endblock content %} {% endblock content %}

View File

@ -7,42 +7,42 @@
{% block content %} {% block content %}
{% element h1 %} {% element h1 %}
{% translate "Enter Email Verification Code" %} {% translate "Enter Email Verification Code" %}
{% endelement %} {% endelement %}
{% setvar email_link %} {% setvar email_link %}
<a href="mailto:{{ email }}">{{ email }}</a> <a href="mailto:{{ email }}">{{ email }}</a>
{% endsetvar %} {% endsetvar %}
{% element p %} {% element p %}
{% blocktranslate %}Weve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} {% blocktranslate %}Weve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %}
{% endelement %} {% endelement %}
{% url 'account_email_verification_sent' as action_url %} {% url 'account_email_verification_sent' as action_url %}
{% element form form=form method="post" action=action_url tags="entrance,email,verification" %} {% element form form=form method="post" action=action_url tags="entrance,email,verification" %}
{% slot body %} {% slot body %}
{% csrf_token %} {% csrf_token %}
{% element fields form=form unlabeled=True %} {% element fields form=form unlabeled=True %}
{% endelement %} {% endelement %}
{{ redirect_field }} {{ redirect_field }}
{% endslot %} {% endslot %}
{% slot actions %} {% slot actions %}
{% element button type="submit" tags="prominent,confirm" %} {% element button type="submit" tags="prominent,confirm" %}
{% translate "Confirm" %} {% translate "Confirm" %}
{% endelement %} {% endelement %}
{% if cancel_url %} {% if cancel_url %}
{% element button href=cancel_url tags="link,cancel" %} {% element button href=cancel_url tags="link,cancel" %}
{% translate "Cancel" %} {% translate "Cancel" %}
{% endelement %} {% endelement %}
{% else %} {% else %}
{% element button type="submit" form="logout-from-stage" tags="link,cancel" %} {% element button type="submit" form="logout-from-stage" tags="link,cancel" %}
{% translate "Cancel" %} {% translate "Cancel" %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% if not cancel_url %} {% if not cancel_url %}
<form id="logout-from-stage" <form id="logout-from-stage"
method="post" method="post"
action="{% url 'account_logout' %}"> action="{% url 'account_logout' %}">
<input type="hidden" name="next" value="{% url 'account_login' %}"> <input type="hidden" name="next" value="{% url 'account_login' %}">
{% csrf_token %} {% csrf_token %}
</form> </form>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View File

@ -52,6 +52,6 @@
{% csrf_token %} {% csrf_token %}
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -7,22 +7,22 @@
{% block content %} {% block content %}
{% element h1 %} {% element h1 %}
{% trans "Email Address" %} {% trans "Email Address" %}
{% endelement %} {% endelement %}
{% if not emailaddresses %} {% if not emailaddresses %}
{% include "account/snippets/warn_no_email.html" %} {% include "account/snippets/warn_no_email.html" %}
{% endif %} {% endif %}
{% url 'account_email' as action_url %} {% url 'account_email' as action_url %}
{% element form method="post" action=action_url %} {% element form method="post" action=action_url %}
{% slot body %} {% slot body %}
{% csrf_token %} {% csrf_token %}
{% if current_emailaddress %} {% if current_emailaddress %}
{% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %}
{% slot label %} {% slot label %}
{% translate "Current email" %}: {% translate "Current email" %}:
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% if new_emailaddress %} {% if new_emailaddress %}
{% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %}
{% slot label %} {% slot label %}
{% if not current_emailaddress %} {% if not current_emailaddress %}
@ -30,33 +30,33 @@
{% else %} {% else %}
{% translate "Changing to" %}: {% translate "Changing to" %}:
{% endif %} {% endif %}
{% endslot %} {% endslot %}
{% slot help_text %} {% slot help_text %}
{% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} {% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %}
{% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} {% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %}
{% trans 'Re-send Verification' %} {% trans 'Re-send Verification' %}
{% endelement %} {% endelement %}
{% if current_emailaddress %} {% if current_emailaddress %}
{% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %}
{% trans 'Cancel Change' %} {% trans 'Cancel Change' %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} {% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %}
{% slot label %} {% slot label %}
{% translate "Change to" %}: {% translate "Change to" %}:
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% slot actions %} {% slot actions %}
{% element button name="action_add" type="submit" %} {% element button name="action_add" type="submit" %}
{% trans "Change Email" %} {% trans "Change Email" %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% if new_emailaddress %} {% if new_emailaddress %}
<form style="display: none" <form style="display: none"
id="pending-email" id="pending-email"
method="post" method="post"
@ -64,5 +64,5 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="email" value="{{ new_emailaddress.email }}"> <input type="hidden" name="email" value="{{ new_emailaddress.email }}">
</form> </form>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View File

@ -204,12 +204,12 @@
<button class="btn btn-support-chat p-0 border border-translucent"> <button class="btn btn-support-chat p-0 border border-translucent">
<span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span> <span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span>
</button> </button>
</div> </div>
</main> </main>
<!-- ===============================================--> <!-- ===============================================-->
<!-- End of Main Content--> <!-- End of Main Content-->
<!-- ===============================================--> <!-- ===============================================-->
<div class="offcanvas offcanvas-end settings-panel border-0" <div class="offcanvas offcanvas-end settings-panel border-0"
id="settings-offcanvas" id="settings-offcanvas"
tabindex="-1" tabindex="-1"
aria-labelledby="settings-offcanvas"> aria-labelledby="settings-offcanvas">
@ -548,8 +548,8 @@
href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/" href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/"
target="_blank">Purchase template</a> target="_blank">Purchase template</a>
</div> </div>
</div> </div>
<a class="card setting-toggle" <a class="card setting-toggle"
href="#settings-offcanvas" href="#settings-offcanvas"
data-bs-toggle="offcanvas"> data-bs-toggle="offcanvas">
<div class="card-body d-flex align-items-center px-2 py-1"> <div class="card-body d-flex align-items-center px-2 py-1">
@ -571,19 +571,19 @@
</div> </div>
<small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small> <small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small>
</div> </div>
</a> </a>
<!-- ===============================================--> <!-- ===============================================-->
<!-- JavaScripts--> <!-- JavaScripts-->
<!-- ===============================================--> <!-- ===============================================-->
<script src="../../../vendors/popper/popper.min.js"></script> <script src="../../../vendors/popper/popper.min.js"></script>
<script src="../../../vendors/bootstrap/bootstrap.min.js"></script> <script src="../../../vendors/bootstrap/bootstrap.min.js"></script>
<script src="../../../vendors/anchorjs/anchor.min.js"></script> <script src="../../../vendors/anchorjs/anchor.min.js"></script>
<script src="../../../vendors/is/is.min.js"></script> <script src="../../../vendors/is/is.min.js"></script>
<script src="../../../vendors/fontawesome/all.min.js"></script> <script src="../../../vendors/fontawesome/all.min.js"></script>
<script src="../../../vendors/lodash/lodash.min.js"></script> <script src="../../../vendors/lodash/lodash.min.js"></script>
<script src="../../../vendors/list.js/list.min.js"></script> <script src="../../../vendors/list.js/list.min.js"></script>
<script src="../../../vendors/feather-icons/feather.min.js"></script> <script src="../../../vendors/feather-icons/feather.min.js"></script>
<script src="../../../vendors/dayjs/dayjs.min.js"></script> <script src="../../../vendors/dayjs/dayjs.min.js"></script>
<script src="../../../assets/js/phoenix.js"></script> <script src="../../../assets/js/phoenix.js"></script>
</body> </body>
</html> </html>

View File

@ -91,11 +91,11 @@
{% trans "Mail me a sign-in code" %} {% trans "Mail me a sign-in code" %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% endelement %} {% endelement %}
{% endif %} {% endif %}
{% if SOCIALACCOUNT_ENABLED %} {% if SOCIALACCOUNT_ENABLED %}
{% include "socialaccount/snippets/login.html" with page_layout="entrance" %} {% include "socialaccount/snippets/login.html" with page_layout="entrance" %}
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}
{% block extra_body %} {% block extra_body %}
{{ block.super }} {{ block.super }}

View File

@ -7,19 +7,19 @@
{% block content %} {% block content %}
{% element h1 %} {% element h1 %}
{% trans "Set Password" %} {% trans "Set Password" %}
{% endelement %} {% endelement %}
{% url 'account_set_password' as action_url %} {% url 'account_set_password' as action_url %}
{% element form method="post" action=action_url %} {% element form method="post" action=action_url %}
{% slot body %} {% slot body %}
{% csrf_token %} {% csrf_token %}
{{ redirect_field }} {{ redirect_field }}
{% element fields form=form %} {% element fields form=form %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% slot actions %} {% slot actions %}
{% element button type="submit" name="action" %} {% element button type="submit" name="action" %}
{% trans 'Set Password' %} {% trans 'Set Password' %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% endblock content %} {% endblock content %}

View File

@ -4,19 +4,19 @@
{% block reauthenticate_content %} {% block reauthenticate_content %}
{% element p %} {% element p %}
{% blocktranslate %}Enter your password:{% endblocktranslate %} {% blocktranslate %}Enter your password:{% endblocktranslate %}
{% endelement %} {% endelement %}
{% url 'account_reauthenticate' as action_url %} {% url 'account_reauthenticate' as action_url %}
{% element form form=form method="post" action=action_url %} {% element form form=form method="post" action=action_url %}
{% slot body %} {% slot body %}
{% csrf_token %} {% csrf_token %}
{% element fields form=form unlabeled=True %} {% element fields form=form unlabeled=True %}
{% endelement %} {% endelement %}
{{ redirect_field }} {{ redirect_field }}
{% endslot %} {% endslot %}
{% slot actions %} {% slot actions %}
{% element button type="submit" tags="primary,reauthenticate" %} {% element button type="submit" tags="primary,reauthenticate" %}
{% trans "Confirm" %} {% trans "Confirm" %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% endblock %} {% endblock %}

View File

@ -7,26 +7,26 @@
{% block content %} {% block content %}
{% element h1 %} {% element h1 %}
{% translate "Mail me a sign-in code" %} {% translate "Mail me a sign-in code" %}
{% endelement %} {% endelement %}
{% element p %} {% element p %}
{% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %} {% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %}
{% endelement %} {% endelement %}
{% url 'account_request_login_code' as login_url %} {% url 'account_request_login_code' as login_url %}
{% element form form=form method="post" action=login_url tags="entrance,login" %} {% element form form=form method="post" action=login_url tags="entrance,login" %}
{% slot body %} {% slot body %}
{% csrf_token %} {% csrf_token %}
{% element fields form=form unlabeled=True %} {% element fields form=form unlabeled=True %}
{% endelement %} {% endelement %}
{{ redirect_field }} {{ redirect_field }}
{% endslot %} {% endslot %}
{% slot actions %} {% slot actions %}
{% element button type="submit" tags="prominent,login" %} {% element button type="submit" tags="prominent,login" %}
{% translate "Request Code" %} {% translate "Request Code" %}
{% endelement %} {% endelement %}
{% endslot %} {% endslot %}
{% endelement %} {% endelement %}
{% url 'account_login' as login_url %} {% url 'account_login' as login_url %}
{% element button href=login_url tags="link" %} {% element button href=login_url tags="link" %}
{% translate "Other sign-in options" %} {% translate "Other sign-in options" %}
{% endelement %} {% endelement %}
{% endblock content %} {% endblock content %}

View File

@ -1,4 +1,4 @@
{% load i18n allauth %} {% load i18n allauth %}
{% element p %} {% element p %}
<strong>{% trans 'Warning:' %}</strong> {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %} <strong>{% trans 'Warning:' %}</strong> {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %}
{% endelement %} {% endelement %}

View File

@ -5,7 +5,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<div class="container py-5"> <div class="container py-5">
<header class="mb-5"> <header class="mb-5">
<h1 class="display-4 fw-bold"> <h1 class="display-4 fw-bold">
@ -42,5 +42,5 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View File

@ -97,7 +97,7 @@
{{ form.as_p }} <!-- Renders the form fields --> {{ form.as_p }} <!-- Renders the form fields -->
<button type="submit">{% trans 'Reset Password' %}</button> <button type="submit">{% trans 'Reset Password' %}</button>
</form> </form>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script> <script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -94,6 +94,7 @@
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th> <th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="list fs-9" id="project-list-table-body"> <tbody class="list fs-9" id="project-list-table-body">
{% for bill_item in itemtxs_qs %} {% for bill_item in itemtxs_qs %}
<tr> <tr>
@ -161,4 +162,4 @@
</div> </div>
<div> <div>
{% include "bill/includes/mark_as.html" %} {% include "bill/includes/mark_as.html" %}
{% endblock %} {% endblock %}

View File

@ -50,7 +50,7 @@
</div> </div>
<!-- Modal Action --> <!-- Modal Action -->
{% modal_action bill 'get' entity_slug %} {% modal_action bill 'get' entity_slug %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-start">
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}" <a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a> class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
{% if perms.django_ledger.change_billmodel %} {% if perms.django_ledger.change_billmodel %}
@ -199,7 +199,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="card-footer p-0"> <div class="card-footer p-0">
<div class="d-flex flex-wrap gap-2 mt-2"> <div class="d-flex flex-wrap gap-2 mt-2 justify-content-end">
<!-- Update Button --> <!-- Update Button -->
{% if perms.django_ledger.change_billmodel %} {% if perms.django_ledger.change_billmodel %}
{% if "update" not in request.path %} {% if "update" not in request.path %}
@ -296,6 +296,15 @@
//document.addEventListener('htmx:afterSwap',processElements); //document.addEventListener('htmx:afterSwap',processElements);
function processElements() { function processElements() {
let bill_status = "{{bill.bill_status}}";
if(bill_status == 'draft'){
const formDiv = document.getElementById('bill-update-form');
if (formDiv) {
setTimeout(()=>{
formDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
},500)
}
}
window.showPOModal = function(title, actionUrl, buttonText) { window.showPOModal = function(title, actionUrl, buttonText) {
const modalEl = document.getElementById('POModal'); const modalEl = document.getElementById('POModal');
if (!modalEl) { if (!modalEl) {

View File

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% load django_ledger %} {% load django_ledger %}
{% load widget_tweaks %} {% load widget_tweaks %}
<form id="bill-update-form" <form id="bill-update-form"
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post"> method="post">
<div class="container-fluid py-4"> <div class="container-fluid py-4">
@ -130,4 +130,4 @@
</div> </div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -55,8 +55,8 @@
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="card-footer d-flex align-items-center gap-2 border-top border-translucent ps-3 pe-4 py-3"> <div class="card-footer d-flex align-items-center gap-2 border-top border-translucent ps-3 pe-4 py-3">
<div class="d-flex align-items-center flex-1 gap-3 border border-translucent rounded-pill px-4"> <div class="d-flex align-items-center flex-1 gap-3 border border-translucent rounded-pill px-4">
<input class="form-control outline-none border-0 flex-1 fs-9 px-0" <input class="form-control outline-none border-0 flex-1 fs-9 px-0"
type="text" type="text"
@ -75,10 +75,10 @@
<button class="btn p-0 border-0 send-btn"> <button class="btn p-0 border-0 send-btn">
<span class="fa-solid fa-paper-plane fs-9"></span> <span class="fa-solid fa-paper-plane fs-9"></span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<button class="btn btn-support-chat p-0 border border-translucent"> <button class="btn btn-support-chat p-0 border border-translucent">
<span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span> <span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span>
</button> </button>
</div> </div>

View File

@ -22,6 +22,7 @@
hx-target="#notesTable" hx-target="#notesTable"
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }" hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }"
hx-swap="outerHTML show:window.top" hx-swap="outerHTML show:window.top"
hx-select-oob="#timeline"
method="post" method="post"
class="add_note_form"> class="add_note_form">
{% csrf_token %} {% csrf_token %}

View File

@ -22,7 +22,7 @@
hx-target=".taskTable" hx-target=".taskTable"
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }" hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select-oob="#toast-container:outerHTML" hx-select-oob="#toast-container:outerHTML,#timeline:outerHTML"
method="post" method="post"
class="add_schedule_form"> class="add_schedule_form">
{% csrf_token %} {% csrf_token %}

View File

@ -335,7 +335,7 @@
</div> </div>
<div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top"> <div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top">
<div class="col-12 col-lg-auto"> <div class="col-12 col-lg-auto">
<div class="timeline-basic mb-9"> <div id="timeline" class="timeline-basic mb-9">
{% for activity in activities %} {% for activity in activities %}
<div class="timeline-item"> <div class="timeline-item">
<div class="row g-3"> <div class="row g-3">
@ -354,6 +354,8 @@
<span class="fa-solid fa-users text-danger fs-8"></span> <span class="fa-solid fa-users text-danger fs-8"></span>
{% elif activity.activity_type == "whatsapp" %} {% elif activity.activity_type == "whatsapp" %}
<span class="fab fa-whatsapp text-success-dark fs-7"></span> <span class="fab fa-whatsapp text-success-dark fs-7"></span>
{% elif activity.activity_type == "meeting" %}
<span class="fa-solid fa-users text-danger fs-8"></span>
{% endif %} {% endif %}
</div> </div>
{% if forloop.last %} {% if forloop.last %}
@ -815,8 +817,8 @@
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
function reset_form() { function reset_form() {
document.querySelector('#id_note').value = "" document.querySelector('#id_note').value = ""
@ -873,4 +875,4 @@
}); });
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -260,4 +260,4 @@
{% url 'lead_create' request.dealer.slug as create_lead_url %} {% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value=empty_state_value url=create_lead_url %} {% include "empty-illustration-page.html" with value=empty_state_value url=create_lead_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -59,6 +59,6 @@
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -3,16 +3,16 @@
<form method="post" <form method="post"
action="{% url 'update_note_to_lead' note.pk %}" action="{% url 'update_note_to_lead' note.pk %}"
enctype="multipart/form-data"> enctype="multipart/form-data">
{% else %} {% else %}
<form method="post" <form method="post"
action="{% url 'add_note_to_lead' lead.slug %}" action="{% url 'add_note_to_lead' lead.slug %}"
enctype="multipart/form-data"> enctype="multipart/form-data">
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
{% if form.instance.pk %} {% if form.instance.pk %}
<button type="submit" class="btn btn-sm btn-phoenix-primary w-100">{{ _("Update") }}</button> <button type="submit" class="btn btn-sm btn-phoenix-primary w-100">{{ _("Update") }}</button>
{% else %} {% else %}
<button type="submit" class="btn btn-sm btn-phoenix-success w-100">{{ _("Add") }}</button> <button type="submit" class="btn btn-sm btn-phoenix-success w-100">{{ _("Add") }}</button>
{% endif %} {% endif %}
</form> </form>

View File

@ -7,24 +7,24 @@
{% block customeCSS %} {% block customeCSS %}
/* General Layout */ /* General Layout */
body { body {
background-color: #f8f9fa; /* Light gray background for contrast */ background-color: #f8f9fa; /* Light gray background for contrast */
} }
/* List Item Styling */ /* List Item Styling */
.list-item { .list-item {
transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out; transition: background-color 0.2s ease-in-out, transform 0.2s ease-in-out;
cursor: pointer; cursor: pointer;
} }
.list-item:hover { .list-item:hover {
background-color: #f1f3f5; background-color: #f1f3f5;
transform: translateY(-2px); /* Subtle lift on hover */ transform: translateY(-2px); /* Subtle lift on hover */
} }
/* Unread Notification Dot */ /* Unread Notification Dot */
.unread-dot { .unread-dot {
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;
@ -32,12 +32,12 @@ body {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
} }
/* HTMX Loading Indicator */ /* HTMX Loading Indicator */
.htmx-request #loading-indicator { .htmx-request #loading-indicator {
display: block; display: block;
} }
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="container-fluid p-5"> <main class="container-fluid p-5">

View File

@ -28,7 +28,7 @@
href="{% url 'estimate_detail' request.dealer.slug opportunity.estimate.pk %}">{{ _("View Quotation") }}</a> href="{% url 'estimate_detail' request.dealer.slug opportunity.estimate.pk %}">{{ _("View Quotation") }}</a>
{% endif %} {% endif %}
{% else %} {% else %}
{% if perms.django_ledger.add_estimatemodel %} {% if perms.django_ledger.add_estimatemodel and not opportunity.estimate %}
<a class="dropdown-item" <a class="dropdown-item"
type="button" type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
@ -629,7 +629,7 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" <div class="tab-pane fade"
id="tab-emails" id="tab-emails"
role="tabpanel" role="tabpanel"
aria-labelledby="emails-tab"> aria-labelledby="emails-tab">
@ -738,9 +738,9 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" <div class="tab-pane fade"
id="tab-activity" id="tab-activity"
hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}" hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}"
hx-trigger="htmx:afterRequest from:" hx-trigger="htmx:afterRequest from:"
@ -813,11 +813,11 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade"
id="updateStageModal" id="updateStageModal"
tabindex="-1" tabindex="-1"
aria-hidden="true"> aria-hidden="true">
@ -846,18 +846,18 @@
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
<!-- email Modal --> <!-- email Modal -->
{% include "components/email_modal.html" %} {% include "components/email_modal.html" %}
<!-- task Modal --> <!-- task Modal -->
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- note Modal --> <!-- note Modal -->
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<div class="modal fade" <div class="modal fade"
id="estimateModal" id="estimateModal"
tabindex="-1" tabindex="-1"
aria-labelledby="estimateModalLabel" aria-labelledby="estimateModalLabel"

View File

@ -181,7 +181,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>
function updateProbabilityValue(value) { function updateProbabilityValue(value) {
const amount = document.getElementById('id_amount'); const amount = document.getElementById('id_amount');
const expectedRevenue = document.getElementById('id_expected_revenue'); const expectedRevenue = document.getElementById('id_expected_revenue');
@ -213,5 +213,5 @@
updateProbabilityValue(rangeInput.value); updateProbabilityValue(rangeInput.value);
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -161,4 +161,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -163,4 +163,4 @@
{% url "customer_create" request.dealer.slug as create_customer_url %} {% url "customer_create" request.dealer.slug as create_customer_url %}
{% include "empty-illustration-page.html" with value=empty_state_value url=create_customer_url %} {% include "empty-illustration-page.html" with value=empty_state_value url=create_customer_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -71,7 +71,7 @@
'#39e6a9', '#4d4f79', '#9f1d35', '#2a5a5b', '#f77f00', '#39e6a9', '#4d4f79', '#9f1d35', '#2a5a5b', '#f77f00',
'#3282b8', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#3282b8', '#00bcd4', '#009688', '#4caf50', '#8bc34a',
'#ffeb3b', '#ff9800', '#ff5722', '#795548', '#9e9e9e' '#ffeb3b', '#ff9800', '#ff5722', '#795548', '#9e9e9e'
]; ];
// Pass translated strings from Django to JavaScript // Pass translated strings from Django to JavaScript
const translatedStrings = { const translatedStrings = {
@ -326,11 +326,11 @@
} }
}); });
} }
{% endif %} {% endif %}
// ----------------------------------------------------------- // -----------------------------------------------------------
// 5. Inventory by Make (Pie Chart) // 5. Inventory by Make (Pie Chart)
// ----------------------------------------------------------- // -----------------------------------------------------------
{% if request.is_dealer or request.is_manager or request.is_inventory %} {% if request.is_dealer or request.is_manager or request.is_inventory %}
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d'); const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
new Chart(ctxInventoryMake, { new Chart(ctxInventoryMake, {
type: 'pie', type: 'pie',

View File

@ -126,4 +126,4 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans "Sales Dashboard" %} {% trans "Sales Dashboard" %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3"> <div class="main-content flex-grow-1 container-fluid mt-4 mb-3">

View File

@ -340,4 +340,4 @@
</tr> </tr>
</table> </table>
</body> </body>
</html> </html>

View File

@ -210,12 +210,12 @@
<button class="btn btn-support-chat p-0 border border-translucent"> <button class="btn btn-support-chat p-0 border border-translucent">
<span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span> <span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span>
</button> </button>
</div> </div>
</main> </main>
<!-- ===============================================--> <!-- ===============================================-->
<!-- End of Main Content--> <!-- End of Main Content-->
<!-- ===============================================--> <!-- ===============================================-->
<div class="offcanvas offcanvas-end settings-panel border-0" <div class="offcanvas offcanvas-end settings-panel border-0"
id="settings-offcanvas" id="settings-offcanvas"
tabindex="-1" tabindex="-1"
aria-labelledby="settings-offcanvas"> aria-labelledby="settings-offcanvas">
@ -532,8 +532,8 @@
href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/" href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/"
target="_blank">Purchase template</a> target="_blank">Purchase template</a>
</div> </div>
</div> </div>
<a class="card setting-toggle" <a class="card setting-toggle"
href="#settings-offcanvas" href="#settings-offcanvas"
data-bs-toggle="offcanvas"> data-bs-toggle="offcanvas">
<div class="card-body d-flex align-items-center px-2 py-1"> <div class="card-body d-flex align-items-center px-2 py-1">
@ -555,19 +555,19 @@
</div> </div>
<small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small> <small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small>
</div> </div>
</a> </a>
<!-- ===============================================--> <!-- ===============================================-->
<!-- JavaScripts--> <!-- JavaScripts-->
<!-- ===============================================--> <!-- ===============================================-->
<script src="../../vendors/popper/popper.min.js"></script> <script src="../../vendors/popper/popper.min.js"></script>
<script src="../../vendors/bootstrap/bootstrap.min.js"></script> <script src="../../vendors/bootstrap/bootstrap.min.js"></script>
<script src="../../vendors/anchorjs/anchor.min.js"></script> <script src="../../vendors/anchorjs/anchor.min.js"></script>
<script src="../../vendors/is/is.min.js"></script> <script src="../../vendors/is/is.min.js"></script>
<script src="../../vendors/fontawesome/all.min.js"></script> <script src="../../vendors/fontawesome/all.min.js"></script>
<script src="../../vendors/lodash/lodash.min.js"></script> <script src="../../vendors/lodash/lodash.min.js"></script>
<script src="../../vendors/list.js/list.min.js"></script> <script src="../../vendors/list.js/list.min.js"></script>
<script src="../../vendors/feather-icons/feather.min.js"></script> <script src="../../vendors/feather-icons/feather.min.js"></script>
<script src="../../vendors/dayjs/dayjs.min.js"></script> <script src="../../vendors/dayjs/dayjs.min.js"></script>
<script src="../../assets/js/phoenix.js"></script> <script src="../../assets/js/phoenix.js"></script>
</body> </body>
</html> </html>

View File

@ -206,12 +206,12 @@
<button class="btn btn-support-chat p-0 border border-translucent"> <button class="btn btn-support-chat p-0 border border-translucent">
<span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span> <span class="fs-8 btn-text text-primary text-nowrap">Chat demo</span><span class="ping-icon-wrapper mt-n4 ms-n6 mt-sm-0 ms-sm-2 position-absolute position-sm-relative"><span class="ping-icon-bg"></span><span class="fa-solid fa-circle ping-icon"></span></span><span class="fa-solid fa-headset text-primary fs-8 d-sm-none"></span><span class="fa-solid fa-chevron-down text-primary fs-7"></span>
</button> </button>
</div> </div>
</main> </main>
<!-- ===============================================--> <!-- ===============================================-->
<!-- End of Main Content--> <!-- End of Main Content-->
<!-- ===============================================--> <!-- ===============================================-->
<div class="offcanvas offcanvas-end settings-panel border-0" <div class="offcanvas offcanvas-end settings-panel border-0"
id="settings-offcanvas" id="settings-offcanvas"
tabindex="-1" tabindex="-1"
aria-labelledby="settings-offcanvas"> aria-labelledby="settings-offcanvas">
@ -528,8 +528,8 @@
href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/" href="https://themes.getbootstrap.com/product/phoenix-admin-dashboard-webapp-template/"
target="_blank">Purchase template</a> target="_blank">Purchase template</a>
</div> </div>
</div> </div>
<a class="card setting-toggle" <a class="card setting-toggle"
href="#settings-offcanvas" href="#settings-offcanvas"
data-bs-toggle="offcanvas"> data-bs-toggle="offcanvas">
<div class="card-body d-flex align-items-center px-2 py-1"> <div class="card-body d-flex align-items-center px-2 py-1">
@ -551,19 +551,19 @@
</div> </div>
<small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small> <small class="text-uppercase text-body-tertiary fw-bold py-2 pe-2 ps-1 rounded-end">customize</small>
</div> </div>
</a> </a>
<!-- ===============================================--> <!-- ===============================================-->
<!-- JavaScripts--> <!-- JavaScripts-->
<!-- ===============================================--> <!-- ===============================================-->
<script src="../../vendors/popper/popper.min.js"></script> <script src="../../vendors/popper/popper.min.js"></script>
<script src="../../vendors/bootstrap/bootstrap.min.js"></script> <script src="../../vendors/bootstrap/bootstrap.min.js"></script>
<script src="../../vendors/anchorjs/anchor.min.js"></script> <script src="../../vendors/anchorjs/anchor.min.js"></script>
<script src="../../vendors/is/is.min.js"></script> <script src="../../vendors/is/is.min.js"></script>
<script src="../../vendors/fontawesome/all.min.js"></script> <script src="../../vendors/fontawesome/all.min.js"></script>
<script src="../../vendors/lodash/lodash.min.js"></script> <script src="../../vendors/lodash/lodash.min.js"></script>
<script src="../../vendors/list.js/list.min.js"></script> <script src="../../vendors/list.js/list.min.js"></script>
<script src="../../vendors/feather-icons/feather.min.js"></script> <script src="../../vendors/feather-icons/feather.min.js"></script>
<script src="../../vendors/dayjs/dayjs.min.js"></script> <script src="../../vendors/dayjs/dayjs.min.js"></script>
<script src="../../assets/js/phoenix.js"></script> <script src="../../assets/js/phoenix.js"></script>
</body> </body>
</html> </html>

View File

@ -35,7 +35,9 @@
<div class="row my-5"> <div class="row my-5">
<div class="card rounded "> <div class="card rounded ">
<div class="card-header "> <div class="card-header ">
<p class="mb-0">{{ _("Group Details") }}</p> <p class="mb-2">{{ _("Group Details") }}</p>
<a class="btn btn-phoenix-secondary me-2" href="{% url 'group_list' request.dealer.slug %}">{% trans "Groups List" %}</a>
<a class="btn btn-phoenix-secondary " href="{% url 'user_list' request.dealer.slug %}">{% trans "Staffs List" %}</a>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -123,4 +125,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,60 +2,56 @@
{% load i18n %} {% load i18n %}
{% load custom_filters %} {% load custom_filters %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block title %} {% block title %}
{% trans "Groups" %} {% trans "Groups" %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main class="py-5"> <div class="container py-5">
<div class="container">
{% if groups or request.GET.q %} {% if groups or request.GET.q %}
<div class=" d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4"> <div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold"> <h1 class="h3 mb-0 text-muted d-flex align-items-center">
<i class="fa-solid fa-user-group fs-3 me-1 "></i>{% trans "Groups" %} <i class="fa-solid fa-user-group me-3 fs-2"></i>
</h5> {% trans "Groups" %}
<div class="d-flex gap-2"> </h1>
<div class="d-flex">
<a href="{% url 'group_create' request.dealer.slug %}" <a href="{% url 'group_create' request.dealer.slug %}"
class="btn btn-phoenix-primary"> class="btn btn-phoenix-primary me-2 shadow-sm">
<i class="fa-solid fa-user-group fs-9 me-1"></i> <i class="fa-solid fa-plus me-2"></i>{% trans "Add New Group" %}
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
</a> </a>
<a href="{% url 'user_list' request.dealer.slug %}" <a href="{% url 'user_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary"> class="btn btn-phoenix-secondary shadow-sm">
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %} <i class="fa-solid fa-arrow-left me-2"></i>{% trans "Back to Staffs" %}
</a> </a>
</div> </div>
</div> </div>
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp"> <div class="card border-0 shadow-sm rounded-lg">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive scrollbar mx-n1 px-1 mt-3"> <div class="table-responsive">
<table class="table align-items-center table-hover mb-0"> <table class="table table-hover table-borderless mb-0">
<thead> <thead class="bg-light">
<tr class=""> <tr>
<th scope="col" class=" text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th> <th class="py-3 px-4">{% trans 'Name'|capfirst %}</th>
<th scope="col" class="text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th> <th class="py-3 px-4">{% trans 'Total Users'|capfirst %}</th>
<th scope="col" class="text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th> <th class="py-3 px-4">{% trans 'Total Permissions'|capfirst %}</th>
<th scope="col" <th class="py-3 px-4 text-center">{% trans 'Actions'|capfirst %}</th>
class="text-uppercase fw-bold text-end pe-4">
{% trans 'actions'|capfirst %}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for group in groups %} {% for group in groups %}
<tr> <tr class="border-bottom">
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td> <td class="align-middle py-3 px-4">{{ group.name }}</td>
<td class="align-middle white-space-nowrap"> <td class="align-middle py-3 px-4 text-muted">
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }} <i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
</td> </td>
<td class="align-middle white-space-nowrap"> <td class="align-middle py-3 px-4 text-muted">
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }} <i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
</td> </td>
<td class="align-middle white-space-nowrap text-end pe-4"> <td class="align-middle py-3 px-4 text-center">
<a class="btn btn-phoenix-secondary btn-sm" <a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'group_detail' request.dealer.slug group.id %}"> href="{% url 'group_detail' request.dealer.slug group.id %}">
<i class="fa-solid fa-eye me-1"></i> <i class="fa-solid fa-eye me-1"></i>{% trans 'View Permissions' %}
{% trans 'view'|capfirst %}
</a> </a>
</td> </td>
</tr> </tr>
@ -65,9 +61,7 @@
</div> </div>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} {% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer bg-light border-top"> <div class="d-flex justify-content-center mt-4">{% include 'partials/pagination.html' %}</div>
<div class="d-flex justify-content-end">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
@ -75,5 +69,4 @@
{% include "empty-illustration-page.html" with value="group" url=create_group_url %} {% include "empty-illustration-page.html" with value="group" url=create_group_url %}
{% endif %} {% endif %}
</div> </div>
</main>
{% endblock %} {% endblock %}

View File

@ -194,7 +194,7 @@
data-bs-parent="#navbarVerticalCollapse" data-bs-parent="#navbarVerticalCollapse"
id="nv-sales"> id="nv-sales">
<li class="collapsed-nav-item-title d-none">{% trans 'sales'|capfirst %}</li> <li class="collapsed-nav-item-title d-none">{% trans 'sales'|capfirst %}</li>
{% if perms.django_ledger.add_estimatemodel %} {% comment %} {% if perms.django_ledger.add_estimatemodel %}
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link" class="nav-link"
@ -204,7 +204,7 @@
</div> </div>
</a> </a>
</li> </li>
{% endif %} {% endif %} {% endcomment %}
{% if perms.django_ledger.view_estimatemodel %} {% if perms.django_ledger.view_estimatemodel %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'estimate_list' request.dealer.slug %}"> <a class="nav-link" href="{% url 'estimate_list' request.dealer.slug %}">
@ -438,10 +438,10 @@
</ul> </ul>
</div> {% endcomment %} </div> {% endcomment %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="navbar-vertical-footer"> <div class="navbar-vertical-footer">
<a class="nav-link ps-2" href="{% if request.is_dealer%}{% url 'ticket_list' request.dealer.slug %} {% else %}#{%endif%}"> <a class="nav-link ps-2" href="{% if request.is_dealer%}{% url 'ticket_list' request.dealer.slug %} {% else %}#{%endif%}">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
{% if user.is_authenticated%} {% if user.is_authenticated%}
@ -452,7 +452,7 @@
{% endif %} {% endif %}
</div> </div>
</a> </a>
</div> </div>
</nav> </nav>
<nav class="navbar navbar-top fixed-top navbar-expand" id="navbarDefault"> <nav class="navbar navbar-top fixed-top navbar-expand" id="navbarDefault">
<div class="collapse navbar-collapse justify-content-between"> <div class="collapse navbar-collapse justify-content-between">
@ -677,8 +677,8 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </nav>

View File

@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'Add Colors' %} {% endblock %} {% trans 'Add Colors' %} {% endblock %}
{% block content %} {% block content %}
<div class="row mt-4 mb-3"> <div class="row mt-4 mb-3">
<h3 class="text-center">{% trans "Add Colors" %}</h3> <h3 class="text-center">{% trans "Add Colors" %}</h3>
<p class="text-center"> <p class="text-center">
@ -117,4 +117,4 @@
width: 100%; width: 100%;
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -75,7 +75,7 @@
</div> </div>
{% endif %} {% endif %}
<!-- Main row --> <!-- Main row -->
{% if car.ready %} {% if car.ready and car.status != 'sold' %}
<div class="d-flex align-items-center gap-2 mb-2"> <div class="d-flex align-items-center gap-2 mb-2">
<button type="button" <button type="button"
class="btn btn-sm btn-phoenix-success" class="btn btn-sm btn-phoenix-success"
@ -91,8 +91,8 @@
<span class="ms-2 badge rounded-pill text-bg-light">{{ active_estimates|length }}</span> <span class="ms-2 badge rounded-pill text-bg-light">{{ active_estimates|length }}</span>
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if perms.inventory.view_car %} {% if perms.inventory.view_car %}
<div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}"> <div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}">
<div class="row g-3 justify-content-between"> <div class="row g-3 justify-content-between">
@ -542,7 +542,7 @@
data-bs-dismiss="modal" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div id="estimateModalBody" class="main-modal-body" style="padding: 20px;"> <div id="estimateModalBody" style="padding: 20px;">
<form action="{% url 'create_estimate_for_car' request.dealer.slug car.slug %}" method="post"> <form action="{% url 'create_estimate_for_car' request.dealer.slug car.slug %}" method="post">
{% csrf_token %} {% csrf_token %}
{{estimate_form|crispy}} {{estimate_form|crispy}}
@ -660,7 +660,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>
@ -772,15 +772,15 @@
document.addEventListener('htmx:afterRequest', e => { document.addEventListener('htmx:afterRequest', e => {
console.log('PUT Success!') console.log('PUT Success!')
}); });
document.addEventListener('htmx:beforeSwap', e => { document.addEventListener('htmx:beforeSwap', e => {
console.log('Before Swap!') console.log('Before Swap!')
}); });
document.addEventListener('htmx:swapError', e => { document.addEventListener('htmx:swapError', e => {
console.log('Swap Error!') console.log('Swap Error!')
}); });
document.addEventListener('htmx:afterSwap', e => { document.addEventListener('htmx:afterSwap', e => {
console.log('After Swap!') console.log('After Swap!')
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -303,10 +303,10 @@
</div> </div>
{% endblock %} {% endblock %}
{% block customJS%} {% block customJS%}
<script> <script>
function toggle_filter() { function toggle_filter() {
const filterContainer = document.querySelector('.filter'); const filterContainer = document.querySelector('.filter');
filterContainer.classList.toggle('d-none'); filterContainer.classList.toggle('d-none');
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -45,7 +45,7 @@
#065535 #065535
#000000 #000000
#133337 #133337
#ffc0cb</a> #ffc0cb</a>
<br> <br>
<span class="glyphicon glyphicon-star"></span>707 <span class="glyphicon glyphicon-star"></span>707
</div> </div>

View File

@ -355,4 +355,4 @@
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@ -7,14 +7,14 @@
{% block content %} {% block content %}
<section class="hero-section text-center py-5"> <section class="hero-section text-center py-5">
<div class="container"> <div class="container">
<h1 class="display-4 fw-bold text-primary">{{ expense.name|title }}</h1> <h1 class="display-4 fw-bold text-primary">{{ expense.name|title }}</h1>
<p class="lead text-muted">{% trans "Comprehensive details for your expense." %}</p> <p class="lead text-muted">{% trans "Comprehensive details for your expense." %}</p>
</div> </div>
</section> </section>
<div class="container my-5"> <div class="container my-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-11"> <div class="col-md-11">
<div class="card shadow-lg border-0 rounded-4"> <div class="card shadow-lg border-0 rounded-4">
@ -93,5 +93,5 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,14 +7,14 @@
{% block content %} {% block content %}
<section class="hero-section text-center py-5"> <section class="hero-section text-center py-5">
<div class="container"> <div class="container">
<h1 class="display-5 fw-bold">{{ service.name|title }}</h1> <h1 class="display-5 fw-bold">{{ service.name|title }}</h1>
<p class=" mt-3">{{ service.description|default:"No description provided." }}</p> <p class=" mt-3">{{ service.description|default:"No description provided." }}</p>
</div> </div>
</section> </section>
<section class="py-5"> <section class="py-5">
<div class="container"> <div class="container">
<div class="card mb-5 p-4 p-lg-5"> <div class="card mb-5 p-4 p-lg-5">
@ -61,6 +61,6 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -78,4 +78,4 @@
{% url 'bank_account_create' request.dealer.slug as create_bank_account_url %} {% url 'bank_account_create' request.dealer.slug as create_bank_account_url %}
{% include "empty-illustration-page.html" with value="bank account" url=create_bank_account_url %} {% include "empty-illustration-page.html" with value="bank account" url=create_bank_account_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -49,6 +49,8 @@
<span class="badge badge-phoenix badge-phoenix-success"> <span class="badge badge-phoenix badge-phoenix-success">
{% elif bill.is_canceled %} {% elif bill.is_canceled %}
<span class="badge badge-phoenix badge-phoenix-danger"> <span class="badge badge-phoenix badge-phoenix-danger">
{% elif bill.is_void %}
<span class="badge badge-phoenix badge-phoenix-secondary">
{% endif %} {% endif %}
{{ bill.bill_status }} {{ bill.bill_status }}
</span> </span>
@ -92,4 +94,4 @@
{% url "bill-create" request.dealer.slug request.entity.slug as create_bill_url %} {% url "bill-create" request.dealer.slug request.entity.slug as create_bill_url %}
{% include "empty-illustration-page.html" with value="bill" url=create_bill_url %} {% include "empty-illustration-page.html" with value="bill" url=create_bill_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -156,4 +156,4 @@
</div> </div>
</div> </div>
<!--test--> <!--test-->
{% endblock %} {% endblock %}

View File

@ -56,4 +56,4 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -293,7 +293,7 @@
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const makeSelect = document.getElementById('make-select'); const makeSelect = document.getElementById('make-select');
const modelSelect = document.getElementById('model-select'); const modelSelect = document.getElementById('model-select');
@ -357,6 +357,6 @@
// Initial call to populate other dropdowns on page load // Initial call to populate other dropdowns on page load
fetchAndUpdateDropdowns(); fetchAndUpdateDropdowns();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
font-size: 0.50rem">{{ notifications_.count }}</span> font-size: 0.50rem">{{ notifications_.count }}</span>
{% endif %} {% endif %}
</div> </div>
<a class="nav-link" <a class="nav-link"
href="{% url 'fetch_notifications' %}" href="{% url 'fetch_notifications' %}"
hx-get="{% url 'fetch_notifications' %}" hx-get="{% url 'fetch_notifications' %}"
hx-swap="innerHTML" hx-swap="innerHTML"

View File

@ -87,9 +87,13 @@
didOpen: (toast) => { didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer; toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer; toast.onmouseleave = Swal.resumeTimer;
} }); }
});
{% with last_notif=notifications_|last %}
let lastNotificationId = {{ last_notif.id|default:0 }};
{% endwith %}
let lastNotificationId = {{ notifications_.last.id|default:0 }};
let seenNotificationIds = new Set(); let seenNotificationIds = new Set();
let counter = document.getElementById('notification-counter'); let counter = document.getElementById('notification-counter');
let notificationsContainer = document.getElementById('notifications-container'); let notificationsContainer = document.getElementById('notifications-container');
@ -100,7 +104,6 @@
let initialUnreadCount = {{ notifications_.count|default:0 }}; let initialUnreadCount = {{ notifications_.count|default:0 }};
updateCounter(initialUnreadCount); updateCounter(initialUnreadCount);
fetchInitialNotifications(); fetchInitialNotifications();
function fetchInitialNotifications() { function fetchInitialNotifications() {
@ -108,29 +111,22 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.notifications && data.notifications.length > 0) { if (data.notifications && data.notifications.length > 0) {
lastNotificationId = data.notifications[0].id; lastNotificationId = data.notifications[0].id;
seenNotificationIds = new Set(); seenNotificationIds = new Set();
let unreadCount = 0; let unreadCount = 0;
data.notifications.forEach(notification => { data.notifications.forEach(notification => {
seenNotificationIds.add(notification.id); seenNotificationIds.add(notification.id);
if (!notification.is_read) { if (!notification.is_read) unreadCount++;
unreadCount++;
}
}); });
renderNotifications(data.notifications); renderNotifications(data.notifications);
updateCounter(unreadCount); updateCounter(unreadCount);
}
// Always connect SSE after initial load
setTimeout(() => { setTimeout(() => {
connectSSE(); connectSSE();
}, 5000); }, 1000);
}
}) })
.catch(error => { .catch(error => {
console.error('Error fetching initial notifications:', error); console.error('Error fetching initial notifications:', error);
@ -143,12 +139,12 @@
eventSource.close(); eventSource.close();
} }
// ✅ FIXED URL HERE
eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId); eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId);
eventSource.addEventListener('notification', function(e) { eventSource.addEventListener('notification', function(e) {
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (seenNotificationIds.has(data.id)) return; if (seenNotificationIds.has(data.id)) return;
seenNotificationIds.add(data.id); seenNotificationIds.add(data.id);
@ -158,6 +154,11 @@
updateCounter('increment'); updateCounter('increment');
if (!notificationsContainer) {
console.warn("Notification container missing, can't render SSE event");
return;
}
const notificationElement = createNotificationElement(data); const notificationElement = createNotificationElement(data);
notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement); notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement);
@ -168,7 +169,7 @@
Toast.fire({ Toast.fire({
icon: 'info', icon: 'info',
html:`${data.message}` html: `${data.message}`
}); });
} catch (error) { } catch (error) {
@ -237,6 +238,8 @@
} }
} }
if (!counter) return;
let currentCount = parseInt(counter.textContent) || 0; let currentCount = parseInt(counter.textContent) || 0;
if (action === 'increment') { if (action === 'increment') {
@ -294,7 +297,7 @@
notificationCard.closest('.notification-card').classList.add('fade-out'); notificationCard.closest('.notification-card').classList.add('fade-out');
setTimeout(() => { setTimeout(() => {
notificationCard.closest('.notification-card').remove(); notificationCard.closest('.notification-card').remove();
}, 200); }, 1000);
} }
} }
}); });

View File

@ -125,7 +125,7 @@
</div> </div>
</div> </div>
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-0 px-1"> <td class="name align-middle white-space-nowrap px-1">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<a class="fs-8 fw-bold" <a class="fs-8 fw-bold"
@ -144,7 +144,7 @@
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> <td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{{ org.created|date }} {{ org.created|date }}
</td> </td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4"> <td class="align-middle white-space-nowrap text-end pe-2 ps-4">
{% if perms.inventory.change_organization or perms.inventory.delete_organization %} {% if perms.inventory.change_organization or perms.inventory.delete_organization %}
<div class="btn-reveal-trigger position-static"> <div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" <button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"

View File

@ -5,7 +5,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5"> <main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-8"> <div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
@ -19,6 +19,7 @@
method="post" class="needs-validation" novalidate> method="post" class="needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<hr class="my-4"> <hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit"> <button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
@ -27,11 +28,11 @@
</button> </button>
<a href="{% url 'dealer_detail' request.dealer.slug %}" <a href="{% url 'dealer_detail' request.dealer.slug %}"
class="btn btn-lg btn-phoenix-secondary md-me-2" class="btn btn-lg btn-phoenix-secondary md-me-2"
type="submit"><i class="fa-solid fa-ban me-1"></i>{{ _("Cancel") }}</a> type="submit"><i class="fa-solid fa-ban me-1"></i>{{ _("Go Back") }}</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
{% endblock %} {% endblock %}

View File

@ -12,4 +12,4 @@
</form> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -54,4 +54,4 @@
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
</form> </form>
{% endblock %} {% endblock %}

View File

@ -12,7 +12,7 @@
</script> </script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold text-gray-800"> <h1 class="text-3xl font-bold text-gray-800">
{% blocktrans with object.id as order_id %}Order #{{ order_id }}{% endblocktrans %} {% blocktrans with object.id as order_id %}Order #{{ order_id }}{% endblocktrans %}
@ -60,15 +60,15 @@
<h2 class="text-xl font-semibold text-gray-800 mb-4"> <h2 class="text-xl font-semibold text-gray-800 mb-4">
<i class="fas fa-credit-card text-gray-400 mr-2"></i> {% trans "Payment" %} <i class="fas fa-credit-card text-gray-400 mr-2"></i> {% trans "Payment" %}
</h2> </h2>
{% if object.completed %} {% if object.completed %}
<div class="bg-green-50 text-green-700 px-4 py-3 rounded-lg flex items-center"> <div class="bg-green-50 text-green-700 px-4 py-3 rounded-lg flex items-center">
<i class="fas fa-check-circle mr-3 text-lg"></i> <i class="fas fa-check-circle mr-3 text-lg"></i>
<p class="text-sm font-medium"> <p class="text-sm font-medium">
{% blocktrans with object.completed as completed %} {% trans "Payment completed on : "%}{{object.completed}}
Payment completed on: {{ completed|date:"F j, Y" }}
{% endblocktrans %}
</p> </p>
</div> </div>
{% else %} {% else %}
{% if object.is_ready_for_payment %} {% if object.is_ready_for_payment %}
<p class="text-gray-600 mb-4"> <p class="text-gray-600 mb-4">
@ -104,5 +104,5 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -19,7 +19,7 @@
const modalBody = document.getElementById('POModalBody') const modalBody = document.getElementById('POModalBody')
modalBody.innerHTML = ` modalBody.innerHTML = `
<div class="d-flex justify-content-center gap-3 py-3"> <div class="d-flex justify-content-center gap-3 py-3">
<a hx-boost="true" class="btn btn-phoenix-primary px-4" href="${actionUrl}"> <a hx-boost="true" class="btn btn-phoenix-danger px-4" href="${actionUrl}">
<i class="fas fa-check-circle me-2"></i>${buttonText} <i class="fas fa-check-circle me-2"></i>${buttonText}
</a> </a>
<button class="btn btn-phoenix-secondary" data-bs-dismiss="modal"> <button class="btn btn-phoenix-secondary" data-bs-dismiss="modal">
@ -45,7 +45,7 @@
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}"
title="Click to view the complete list of Purchase Orders" title="Click to view the complete list of Purchase Orders"
role="button"> role="button">
<i class="fas fa-list me-2"></i>{% trans 'PO List' %} <i class="fas fa-list me-2"></i>{% trans 'Purchase Order List' %}
</a> </a>
</p> </p>
</div> </div>
@ -135,18 +135,34 @@
{% endif %} {% endif %}
{% if po_model.can_delete %} {% if po_model.can_delete %}
{% if perms.django_ledger.delete_purchaseordermodel %} {% if perms.django_ledger.delete_purchaseordermodel %}
<button class="btn btn-phoenix-danger" <button type="button" class="btn btn-phoenix-danger" data-bs-toggle="modal" data-bs-target="#exampleModal">
onclick="showPOModal('Delete PO', '{% url 'po-delete' request.dealer.slug entity_slug po_model.pk %}', 'Delete')"> {% trans "Delete" %}
<i class="fas fa-trash me-2"></i>{% trans 'Delete' %}
</button> </button>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{% trans "Purchase Order Delete" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% include 'purchase_orders/po_confirm_delete.html' %}
</div>
</div>
</div>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if po_model.can_void %} {% comment %} TODO: upgrade djnago ledger or replace core functionality {% endcomment %}
{% comment %} issue with django ledger base functionality when marking as void throughs error , will be fix in future {% endcomment %}
{% comment %} {% if po_model.can_void %}
<button class="btn btn-phoenix-danger" <button class="btn btn-phoenix-danger"
onclick="showPOModal('Void PO', '{% url 'po-action-mark-as-void' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Void')"> onclick="showPOModal('{% trans "Void Purchase Order" %}', '{% url 'po-action-mark-as-void' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Void')">
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %} <i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
</button> </button>
{% endif %} {% endif %} {% endcomment %}
{% if po_model.can_cancel %} {% if po_model.can_cancel %}
<button class="btn btn-phoenix-secondary" <button class="btn btn-phoenix-secondary"
onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Cancelled')"> onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Cancelled')">

View File

@ -32,8 +32,8 @@
</button> </button>
{% endif %} {% endif %}
</th> </th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'Quantity' %}</th> <th>{% trans 'Quantity' %}</th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'Unit' %}</th> <th>{% trans 'Unit' %}</th>
<th class="text-end">{% trans 'Amount' %}</th> <th class="text-end">{% trans 'Amount' %}</th>
<th>{% trans 'Status' %}</th> <th>{% trans 'Status' %}</th>
@ -52,8 +52,8 @@
{{ f.item_model|add_class:"form-control" }} {{ f.item_model|add_class:"form-control" }}
{% if f.errors %}<div class="text-danger small">{{ f.errors }}</div>{% endif %} {% if f.errors %}<div class="text-danger small">{{ f.errors }}</div>{% endif %}
</td> </td>
<td id="{{ f.instance.html_id_unit_cost }}">{{ f.po_unit_cost|add_class:"form-control" }}</td>
<td id="{{ f.instance.html_id_quantity }}">{{ f.po_quantity|add_class:"form-control" }}</td> <td id="{{ f.instance.html_id_quantity }}">{{ f.po_quantity|add_class:"form-control" }}</td>
<td id="{{ f.instance.html_id_unit_cost }}">{{ f.po_unit_cost|add_class:"form-control" }}</td>
<td>{{ f.entity_unit|add_class:"form-control" }}</td> <td>{{ f.entity_unit|add_class:"form-control" }}</td>
<td class="text-end" id="{{ f.instance.html_id_total_amount }}"> <td class="text-end" id="{{ f.instance.html_id_total_amount }}">
<span class="currency">{{ CURRENCY }}</span>{{ f.instance.po_total_amount | currency_format }} <span class="currency">{{ CURRENCY }}</span>{{ f.instance.po_total_amount | currency_format }}

View File

@ -1,17 +1,12 @@
<!-- templates/purchase_orders/po_confirm_delete.html --> {% load i18n %}
{% extends "base.html" %} <form action="{% url 'po-delete' dealer_slug entity_slug po_model.pk %}" method="post">
{% block title %}{% trans "Confirm Delete"%} - {{ block.super }}{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>{% trans "Confirm Deletion" %}</h2>
<p>
{% trans "Are you sure you want to delete the Purchase Order" %} <strong>"{{ po.po_number }}"</strong>?
</p>
<form method="post">
{% csrf_token %} {% csrf_token %}
<p>
{% blocktrans with po_number=po_model.po_number %}
Are you sure you want to delete #{{ po_number }}?
{% endblocktrans %}
</p>
<button type="submit" class="btn btn-phoenix-danger">{% trans "Yes, Delete" %}</button> <button type="submit" class="btn btn-phoenix-danger">{% trans "Yes, Delete" %}</button>
<a href="{% url 'purchase_order_detail' po.id %}" <a href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug po_model.pk %}"
class="btn btn-phoenix-secondary">{% trans "Cancel" %}</a> class="btn btn-phoenix-secondary">{% trans "No" %}</a>
</form> </form>
</div>
{% endblock %}

View File

@ -1,35 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load django_ledger %}
{% block title %}
{% trans "Delete Purchase Order" %}
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<form action="{% url 'po-delete' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"
method="post">
{% csrf_token %}
<div class="card shadow">
<div class="card-body text-center py-4">
<h2 class="card-title h3 fw-light mb-4">
{% blocktrans %}Are you sure you want to delete
Purchase Order {{ po_model.po_number }}?{% endblocktrans %}
</h2>
<p class="card-text text-muted mb-4">
{% trans "All transactions associated with this Purchase Order will be deleted.
If you want to void the PO instead," %} <a href="{% url 'purchase_order_detail' dealer_slug=request.dealer.slug pk=po_model.uuid %}"
class="text-decoration-none">{% trans "click here" %}</a>
</p>
<div class="d-flex justify-content-center gap-3 mt-4">
<a href="{% url 'purchase_order_update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug po_pk=po_model.uuid %}"
class="btn btn-phoenix-primary px-4">{% trans 'Go Back' %}</a>
<button type="submit" class="btn btn-phoenix-danger px-4">{% trans 'Delete' %}</button>
</div>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -19,9 +19,9 @@
{% if po_model.po_status == 'fulfilled' %} {% if po_model.po_status == 'fulfilled' %}
<div class="ms-2"> <div class="ms-2">
<a class="btn btn-phoenix-primary my-2 mx-2" <a class="btn btn-phoenix-primary my-2 mx-2"
href="{{ request.path }}?format=pdf&lang=en"><i class="fa-solid fa-arrow-down me-1"></i>{% trans 'Download PO ENG' %}</a> href="{{ request.path }}?format=pdf&lang=en"><i class="fa-solid fa-arrow-down me-1"></i>{% trans 'Download PO EN' %}</a>
<a class="btn btn-phoenix-primary my-2" <a class="btn btn-phoenix-primary my-2"
href="{{ request.path }}?format=pdf&lang=ar"><i class="fa-solid fa-arrow-down me-1"></i>{% trans 'Download PO ARB' %}</a> href="{{ request.path }}?format=pdf&lang=ar"><i class="fa-solid fa-arrow-down me-1"></i>{% trans 'Download PO AR' %}</a>
</diV> </diV>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,9 +1,9 @@
{% load tenhal_tag %} {% load tenhal_tag %}
{% load custom_filters %} {% load custom_filters %}
{% load i18n static custom_filters num2words_tags %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ar" dir="rtl"> <html lang="ar" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ po_model.po_number }}</title> <title>{{ po_model.po_number }}</title>
<style> <style>
@ -104,17 +104,22 @@
font-weight: bold; font-weight: bold;
} }
.text-right { .text-right {
text-align: left; /* This is now for left alignment in an RTL context */ text-align: right; /* This is now for left alignment in an RTL context */
} }
/* Footer Styles */ /* Footer Styles */
.document-footer { .document-footer {
position: relative;
bottom: 0;
left: 40px;
width: 100%;
text-align: center; text-align: center;
font-size: 10px; font-size: 10px;
color: #888; color: #888;
border-top: 1px solid #ddd;
padding-top: 15px; padding-top: 15px;
margin-top: 30px; margin: 0 20mm;
} }
.footer-flex { .footer-flex {
display: flex; display: flex;
@ -122,37 +127,76 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
} }
.footer-logo {
text-align: center;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="document-header"> <div class="document-header">
<div>
<h1>{{ dealer.name }}</h1>
<address>
العنوان&nbsp;:&nbsp;{{ dealer.address }}<br>
البريد الإلكتروني&nbsp;:&nbsp;{{ dealer.user.email }}<br>
الهاتف&nbsp;:&nbsp;{{ dealer.phone_number }}<br>
رقم السجل التجاري&nbsp;:&nbsp;{{ dealer.crn }} &nbsp;|&nbsp; رقم ضريبة القيمة المضافة&nbsp;:&nbsp;{{ dealer.vrn }}
</address>
</div>
<div>
<div class="dealer-logo ">
{% if dealer.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div> <div>
<h1>أمر شراء</h1> <h1>أمر شراء</h1>
<h2 style="font-size: 18px;">{{ po_model.po_number }}</h2> <h2 style="font-size: 18px;">{{ po_model.po_number }}</h2>
</div> </div>
<div>
<h1>{{ dealer.name }}</h1>
<address>
العنوان: {{ dealer.address }}<br>
البريد الإلكتروني: {{ dealer.user.email }}<br>
الهاتف: {{ dealer.phone_number }}<br>
رقم السجل التجاري: {{ dealer.crn }} &nbsp;|&nbsp; رقم ضريبة القيمة المضافة: {{ dealer.vrn }}
</address>
</div>
</div> </div>
<div class="document-details"> <div class="document-details">
<div class="section"> <div class="section">
<h2>التفاصيل:</h2> <h2>التفاصيل:</h2>
<p><span class="label">رقم أمر الشراء:</span> {{ po_model.po_number }}</p> <p><span class="label">رقم أمر الشراء&nbsp;:&nbsp;</span> {{ po_model.po_number }}</p>
<p><span class="label">تاريخ الإصدار:</span> {{ po_model.date_fulfilled|date:"Y/m/d" }}</p> <p><span class="label">تاريخ الإصدار&nbsp;:&nbsp;</span> {{ po_model.date_fulfilled|date:"Y/m/d" }}</p>
</div> </div>
<div class="section"> <div class="section">
<h2>يُرسل إلى:</h2> <h2>يُرسل إلى:</h2>
<p><span class="label">المورد:</span> {{ vendor.vendor_name }}</p> <p><span class="label">المورد&nbsp;:&nbsp;</span> {{ vendor.vendor_name }}</p>
<p><span class="label">البريد الإلكتروني:</span> {{ vendor.email }}</p> <p><span class="label">البريد الإلكتروني&nbsp;:&nbsp;</span> {{ vendor.email }}</p>
<p><span class="label">الهاتف:</span> {{ vendor.phone }}</p> <p><span class="label">الهاتف&nbsp;:&nbsp;</span> {{ vendor.phone }}</p>
<p><span class="label">العنوان:</span> {{ vendor.address_1 }}</p> <p><span class="label">العنوان&nbsp;:&nbsp;</span> {{ vendor.address_1 }}</p>
</div> </div>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-12">
@ -161,22 +205,28 @@
</div> </div>
</div> </div>
<div class="document-details" style="margin-top: 30px;">
<div class="section" style="width: 100%;">
<h2>ملاحظات:</h2>
<p>{{ po_model.notes|default:"لا توجد ملاحظات إضافية." }}</p>
</div>
</div>
<div class="document-details" style="margin-top: 10px; border-top: 1px solid #ddd; padding-top: 10px;"> <div class="document-details" style="margin-top: 10px; border-top: 1px solid #ddd; padding-top: 10px;">
<div class="section text-right" style="width: 100%;"> <div class="section text-right" style="width: 100%;">
<p class="h4"><span class="label">المبلغ الإجمالي:</span> {{ po_total_amount|floatformat:'2g' }}<span class="icon-saudi_riyal"></span></p> <p class="h4"><span class="label">المبلغ الإجمالي&nbsp;:&nbsp;</span> {{ po_total_amount|currency_format }}<span class="icon-saudi_riyal"></span></p>
</div> </div>
</div> </div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer footer-flex"> <div class="document-footer">
<p>&copy;&nbsp;<strong>هيكل</strong>&nbsp;{% now "Y" %}&nbsp;جميع الحقوق محفوظة.</p> <div class="footer-logo">
<p><strong>تنحل</strong>مدعوم من</p> <img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div> </div>
</body> <div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div>
</div>
</body>
</html> </html>

View File

@ -106,4 +106,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,7 +6,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ po_model.po_number }}</title> <title>{{ po_model.po_number }}</title>
<style> <style>
@ -100,38 +100,75 @@
font-weight: bold; font-weight: bold;
} }
.text-right { .text-right {
text-align: right; text-align: left;
} }
/* Footer Styles */ /* Footer Styles */
.document-footer { .document-footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
bottom: 0;
left: 0;
width: 100%;
text-align: center; text-align: center;
font-size: 10px; font-size: 10px;
color: #888; color: #888;
border-top: 1px solid #ddd;
padding-top: 15px; padding-top: 15px;
margin-top: 30px; margin: 0 auto; /* This change centers the footer container */
} }
.footer-flex { .footer-logo {
display: flex; text-align: center;
justify-content: space-between;
align-items: center;
width: 100%;
} }
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="document-header"> <div class="document-header">
<div> <div>
<h1>{{ dealer.name }}</h1> <h1>{{ dealer.name }}</h1>
<address> <address>
Address: {{ dealer.address}}<br> Address&nbsp;:&nbsp;{{ dealer.address}}<br>
Email: {{ dealer.user.email }}<br> Email&nbsp;:&nbsp;{{ dealer.user.email }}<br>
Phone: {{dealer.phone_number }}<br> Phone&nbsp;:&nbsp;{{dealer.phone_number }}<br>
CRN: {{dealer.crn}}&nbsp;&nbsp;|&nbsp;VRN: {{dealer.vrn}} CRN&nbsp;:&nbsp;{{dealer.crn}}&nbsp;&nbsp;|&nbsp;VRN&nbsp;:&nbsp;{{dealer.vrn}}
</address> </address>
</div> </div>
<div>
<div class="dealer-logo ">
{% if dealer.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div> <div>
<h1>PURCHASE ORDER</h1> <h1>PURCHASE ORDER</h1>
<h2 style="font-size: 18px;">{{ po_model.po_number }}</h2> <h2 style="font-size: 18px;">{{ po_model.po_number }}</h2>
@ -141,15 +178,15 @@
<div class="document-details"> <div class="document-details">
<div class="section"> <div class="section">
<h2>BILL TO:</h2> <h2>BILL TO:</h2>
<p><span class="label">Vendor: {{vendor.vendor_name}}</span> </p> <p><span class="label">Vendor&nbsp;:&nbsp;{{vendor.vendor_name}}</span> </p>
<p><span class="label">Email: {{vendor.email}}</span> </p> <p><span class="label">Email&nbsp;:&nbsp;{{vendor.email}}</span> </p>
<p><span class="label">Phone: {{vendor.phone}}</span> </p> <p><span class="label">Phone&nbsp;:&nbsp;{{vendor.phone}}</span> </p>
<p><span class="label">Address: {{vendor.address_1}}</span> </p> <p><span class="label">Address&nbsp;:&nbsp;{{vendor.address_1}}</span> </p>
</div> </div>
<div class="section"> <div class="section">
<h2>DETAILS:</h2> <h2>DETAILS:</h2>
<p><span class="label">PO Number:</span> {{ po_model.po_number }}</p> <p><span class="label">PO Number&nbsp;:&nbsp;</span> {{ po_model.po_number }}</p>
<p><span class="label">Issue Date:</span> {{ po_model.date_fulfilled|date:"F j, Y" }}</p> <p><span class="label">Issue Date&nbsp;:&nbsp;</span> {{ po_model.date_fulfilled|date:"F j, Y" }}</p>
</div> </div>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-12">
@ -161,14 +198,25 @@
<div class="document-details" style="margin-top: 30px;"> <div class="document-details" style="margin-top: 30px;">
<div class="section text-right"> <div class="section text-right">
<p class="h4"><span class="label">Total Amount:</span> {{ po_total_amount|floatformat:'2g' }}<span class="icon-saudi_riyal"></span></p> <p class="h4"><span class="label">Total Amount&nbsp;:&nbsp;</span> {{ po_total_amount|currency_format }}<span class="icon-saudi_riyal"></span></p>
</div>
</div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div>
<div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div> </div>
</div> </div>
<div class="document-footer footer-flex"> </body>
<p>&copy;&nbsp;{% now "Y" %}&nbsp;All rights reserved&nbsp;<strong>Haikal</strong>&nbsp;</p>
<p>Powered By&nbsp;<strong>Tenhal</strong></p>
</div>
</body>
</html> </html>

View File

@ -124,4 +124,4 @@
{% include "message-illustration.html" with value1=_("You don't have a Vendor, Please add a Vendor before creating a Purchase Order.") value2=_("Create New Vendor") url=vendor_create_url %} {% include "message-illustration.html" with value1=_("You don't have a Vendor, Please add a Vendor before creating a Purchase Order.") value2=_("Create New Vendor") url=vendor_create_url %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -58,4 +58,4 @@
</table> </table>
</div> </div>
<div> <div>
{% endblock content %} {% endblock content %}

View File

@ -6,7 +6,7 @@
<tr class="has-text-centered bg-body-highlight"> <tr class="has-text-centered bg-body-highlight">
<th>البند</th> <th>البند</th>
<th>سعر الوحدة</th> <th>سعر الوحدة</th>
<th>كمية أمر الشراء</th> <th>الكمية</th>
<th>المبلغ</th> <th>المبلغ</th>
</tr> </tr>
</thead> </thead>
@ -14,7 +14,7 @@
{% for item in po_items_list %} {% for item in po_items_list %}
<tr> <tr>
<td>{{ item.item_model }}</td> <td>{{ item.item_model }}</td>
<td class="has-text-centered">{{ item.po_unit_cost }}</td> <td class="has-text-centered">{{ item.po_unit_cost|currency_format }}</td>
<td class="has-text-centered">{{ item.po_quantity }}</td> <td class="has-text-centered">{{ item.po_quantity }}</td>
<td class="has-text-centered"> <td class="has-text-centered">
<span class="icon-saudi_riyal"></span>{{ item.po_total_amount | currency_format }} <span class="icon-saudi_riyal"></span>{{ item.po_total_amount | currency_format }}
@ -26,12 +26,12 @@
<tfoot> <tfoot>
<tr> <tr>
<td>إجمالي مبلغ أمر الشراء</td>
<td></td> <td></td>
<td class="has-text-left">إجمالي مبلغ أمر الشراء</td> <td class="has-text-left">{{po_quantity}}</td>
<td class="has-text-weight-bold has-text-centered"> <td class="has-text-weight-bold has-text-centered">
<span class="icon-saudi_riyal"></span>{{ po_model.po_amount | currency_format }} <span class="icon-saudi_riyal"></span>{{ po_model.po_amount | currency_format }}
</td> </td>
<td></td>
</tr> </tr>
</tfoot> </tfoot>

View File

@ -14,7 +14,7 @@
{% for item in po_items_list %} {% for item in po_items_list %}
<tr> <tr>
<td>{{ item.item_model }}</td> <td>{{ item.item_model }}</td>
<td class="has-text-centered">{{ item.po_unit_cost }}</td> <td class="has-text-centered">{{ item.po_unit_cost|currency_format}}</td>
<td class="has-text-centered">{{ item.po_quantity }}</td> <td class="has-text-centered">{{ item.po_quantity }}</td>
<td class="has-text-centered"> <td class="has-text-centered">
<span class="icon-saudi_riyal"></span>{{ item.po_total_amount | currency_format }} <span class="icon-saudi_riyal"></span>{{ item.po_total_amount | currency_format }}
@ -26,12 +26,14 @@
<tfoot> <tfoot>
<tr> <tr>
<td></td> <td>Total PO Amount</td>
<td class="has-text-right">Total PO Amount</td> <td class="has-text-right"></td>
<td>{{po_quantity}}</td>
<td class="has-text-weight-bold has-text-centered"> <td class="has-text-weight-bold has-text-centered">
<span class="icon-saudi_riyal"></span>{{ po_model.po_amount | currency_format }} <span class="icon-saudi_riyal"></span>{{ po_model.po_amount | currency_format }}
</td> </td>
<td></td>
</tr> </tr>
</tfoot> </tfoot>

View File

@ -47,8 +47,8 @@
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
<!-- Results table --> <!-- Results table -->
<div class="table-responsive mt-3">{% include "recalls/partials/recall_cars_table.html" %}</div> <div class="table-responsive mt-3">{% include "recalls/partials/recall_cars_table.html" %}</div>
{% endblock content %} {% endblock content %}

View File

@ -134,11 +134,17 @@
{% if perms.django_ledger.change_estimatemodel %} {% if perms.django_ledger.change_estimatemodel %}
<a href="{% url 'send_email' request.dealer.slug estimate.pk %}" <a href="{% url 'send_email' request.dealer.slug estimate.pk %}"
class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-sm-2"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a> class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-sm-2"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a>
<a href="{% url 'estimate_print' request.dealer.slug estimate.pk %}" <a href="{% url 'estimate_print' request.dealer.slug estimate.pk %}?lang=en"
class="btn btn-phoenix-secondary" class="btn btn-phoenix-secondary"
target="_blank"> target="_blank">
<span class="d-none d-sm-inline-block"><i class="fas fa-print me-2"></i>{% trans 'Print' %}</span> <span class="d-none d-sm-inline-block"><i class="fas fa-print me-2"></i>{% trans 'Print EN' %}</span>
</a> </a>
<a href="{% url 'estimate_print' request.dealer.slug estimate.pk %}?lang=ar"
class="btn btn-phoenix-secondary"
target="_blank">
<span class="d-none d-sm-inline-block"><i class="fas fa-print me-2"></i>{% trans 'Print AR' %}</span>
</a>
{% endif %} {% endif %}
{% if estimate.sale_orders.first %} {% if estimate.sale_orders.first %}
<!--if sale order exist--> <!--if sale order exist-->
@ -287,8 +293,9 @@
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td> <td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td>
<td class="align-middle text-start fw-semibold"> <td class="align-middle text-start fw-semibold">
{% for service in data.additional_services.services %}
<small><span class="fw-semibold">+ {{ service.0.name }} - {{ service.0.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small> {% for service in additional_finances %}
<small><span class="fw-semibold">+ {{ service.name }} - {{ service.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small>
<br> <br>
{% endfor %} {% endfor %}
{% if estimate.is_draft %} {% if estimate.is_draft %}
@ -304,20 +311,20 @@
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="7">{% trans "Grand Total" %}</td> <td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="7">{% trans "Grand Total" %}</td>
<td class="align-middle text-start fw-bolder"> <td class="align-middle text-start fw-bolder">
<span id="grand-total">{{ data.grand_total|floatformat }}<span class="icon-saudi_riyal"></span></span> <span id="grand-total">{{ grand_total|floatformat }}<span class="icon-saudi_riyal"></span></span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<!-- end of .row--> <!-- end of .row-->
</section> </section>
<!-- <section> close ============================--> <!-- <section> close ============================-->
<!-- ============================================--> <!-- ============================================-->
<!-- add update Modal --> <!-- add update Modal -->
<div class="modal fade" <div class="modal fade"
id="additionalModal" id="additionalModal"
tabindex="-1" tabindex="-1"
aria-labelledby="additionalModalLabel" aria-labelledby="additionalModalLabel"
@ -342,7 +349,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>

View File

@ -1,264 +0,0 @@
{% load i18n static custom_filters num2words_tags %}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>{% trans "Quotation" %}</title>
<style>
/* General Body and Font Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
}
/* Page Layout and Margins for PDF */
@page {
size: A4;
margin: 20mm;
@top-left {
content: "صفحة " counter(page) " من " counter(pages);
font-size: 10px;
color: #555;
}
}
/* Header Styles */
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3; /* A professional blue */
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
}
/* Document Details Section */
.document-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;
}
/* Footer Styles */
.document-footer {
text-align: center;
font-size: 10px;
color: #888;
border-top: 1px solid #ddd;
padding-top: 15px;
margin-top: 30px;
}
.footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
</head>
<body>
<div class="document-header">
<div>
<h1>{{ request.dealer.name }}</h1>
<address>
العنوان: {{ request.dealer.address }}<br>
البريد الإلكتروني: {{ request.dealer.user.email }}<br>
الهاتف: {{ request.dealer.phone_number }}<br>
رقم السجل التجاري: {{ request.dealer.crn }}&nbsp;&nbsp;|&nbsp;الرقم الضريبي: {{ request.dealer.vrn }}
</address>
</div>
<div>
<h1>عرض سعر</h1>
<h2 style="font-size: 18px;">{{ estimate.estimate_number }}</h2>
</div>
</div>
<div class="document-details">
<div class="section">
<h2>{{ estimate.customer.customer_name }}: إلى</h2>
<p><span class="label">العميل: {{ estimate.customer.customer_name }}</span></p>
<p><span class="label">البريد الإلكتروني: {{ estimate.customer.email |default:"N/A"}}</span></p>
<p><span class="label">الهاتف: {{ estimate.customer.phone_number|default:"N/A" }}</span></p>
<p><span class="label">العنوان: {{ estimate.customer.address_1|default:"N/A" }}</span></p>
</div>
<div class="section text-left">
<h2>التفاصيل:</h2>
<p><span class="label">رقم عرض السعر:</span> {{ estimate.estimate_number }}</p>
<p><span class="label">تاريخ الإصدار:</span> {{ estimate.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">طريقة الدفع:</span> {{ estimate.get_terms_display }}</p>
</div>
</div>
<div class="col-lg-12">
<div class="table-responsive">
<div class="d-flex justify-content-between">
<span class="fs-9 fw-thin">تفاصيل السيارة</span>
</div>
<table class="table table-sm table-bordered m-1">
<thead>
<tr>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الصانع</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الموديل</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">السلسلة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الفئة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">السنة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">رقم الهيكل</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الكمية</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">سعر الوحدة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الخصم</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الضريبة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">الإجمالي</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_make.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_model.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_serie.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center fs-10 align-content-center">{{ data.car.year }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.vin }}</td>
<td class="text-center fs-10 align-content-center">1</td>
<td class="text-center fs-10 align-content-center">{{ data.car.marked_price|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.discount_amount|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.vat_amount|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.final_price|floatformat:2 }}</td>
</tr>
</tbody>
</table>
</div>
</div>
{% if data.additional_services %}
<div class="col-lg-12">
<div class="table-responsive">
<div class="d-flex justify-content-between">
<span class="fs-9 fw-thin">الخدمات الإضافية</span>
</div>
<table class="table table-sm table-bordered m-1">
<thead>
<tr>
<th class="text-center fs-10 align-content-center">النوع</th>
<th class="text-center fs-10 align-content-center">القيمة</th>
<th class="text-center fs-10 align-content-center">ضريبة الخدمة</th>
<th class="text-center fs-10 align-content-center">الإجمالي</th>
</tr>
</thead>
<tbody>
{% for service in data.additional_services.services %}
<tr>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.0.name }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.0.price|floatformat:2 }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.1|floatformat:2 }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.0.price_|floatformat:2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="document-details" style="margin-top: 30px;">
<div class="section text-left">
<p><span class="label">إجمالي ضريبة القيمة المضافة:</span> {{ data.total_vat|floatformat:'2g' }}</p>
<p><span class="label">الإجمالي الكلي:</span> {{ data.grand_total|floatformat:'2g' }}</p>
<p><span class="label">كتابةً:</span> {{ data.grand_total|num_to_words }}</p>
</div>
</div>
<div class="document-footer footer-flex">
<p>&copy;&nbsp;{% now "Y" %}&nbsp;جميع الحقوق محفوظة&nbsp;<strong>هيكل</strong></p>
<p>بواسطة&nbsp;<strong>تنحل</strong></p>
</div>
</body>
</html>

View File

@ -0,0 +1,357 @@
{% load i18n static custom_filters num2words_tags %}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>عرض سعر</title>
<style>
/* General Body and Font Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
position: relative;
}
/* Page Layout and Margins for PDF */
@page {
size: A4;
margin: 20mm;
@top-left {
content: "صفحة " counter(page) " من " counter(pages);
font-size: 10px;
color: #555;
}
}
/* Header Styles */
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3;
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
}
/* Document Details Section */
.document-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 80%;
border-collapse: collapse;
margin: 20px auto; /* Centering the table */
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;2px solid #333;
}
/* Responsive and layout helpers */
.table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.table-header span {
font-size: 9px;
font-weight: lighter;
}
/* Footer Styles */
.document-footer {
position: relative;
bottom: 0;
left: 40px;
width: 100%;
text-align: center;
font-size: 10px;
color: #888;
padding-top: 15px;
margin: 0 20mm;
}
.footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.footer-logo {
text-align: center;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style>
</head>
<body>
<div class="document-header">
<div>
<h1>{{ dealer_info.name }}</h1>
<address>
العنوان&nbsp;:&nbsp;{{ dealer_info.address }}<br>
البريد الإلكتروني&nbsp;:&nbsp;{{ dealer_info.user.email }}<br>
الهاتف&nbsp;:&nbsp;{{ dealer_info.phone_number }}<br>
رقم السجل التجاري&nbsp;:&nbsp;{{dealer_info.crn }}<br>
الرقم الضريبي&nbsp;:&nbsp;{{ dealer_info.vrn }}
</address>
</div>
<div>
<div class="dealer-logo ">
{% if dealer_info.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer_info.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div>
<h1>عرض سعر</h1>
<h2 style="font-size: 18px;">{{ estimate.estimate_number }}</h2>
</div>
</div>
<div class="document-details">
<div class="section">
<h2>{{ customer_obj.full_name }}: إلى</h2>
<p><span class="label">العميل&nbsp;:&nbsp;{{ customer_obj.full_name }}</span></p>
<p><span class="label">البريد الإلكتروني&nbsp;:&nbsp;{{ customer_obj.email |default:"N/A"}}</span></p>
<p><span class="label">الهاتف&nbsp;:&nbsp;{{ customer_obj.phone_number|default:"N/A" }}</span></p>
<p><span class="label">العنوان&nbsp;:&nbsp;{{ customer_obj.address|default:"N/A" }}</span></p>
</div>
<div class="section text-left">
<h2>التفاصيل:</h2>
<p><span class="label">رقم عرض السعر&nbsp;:&nbsp;</span> {{estimate.estimate_number }}</p>
<p><span class="label">تاريخ الإصدار&nbsp;:&nbsp;</span> {{ estimate.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">طريقة الدفع&nbsp;:&nbsp;</span> {{ estimate.get_terms_display }}</p>
</div>
</div>
<div class="table-container">
<div class="table-header">
<span>تفاصيل السيارة</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>الصانع</span>
</th>
<th class="text-center">
<span>الموديل</span>
</th>
<th class="text-center">
<span>السلسلة</span>
</th>
<th class="text-center">
<span>الفئة</span>
</th>
<th class="text-center">
<span>السنة</span>
</th>
<th class="text-center">
<span>رقم الهيكل</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">{{ data.car.id_car_make.name }}</td>
<td class="text-center">{{ data.car.id_car_model.name }}</td>
<td class="text-center">{{ data.car.id_car_serie.name }}</td>
<td class="text-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center">{{ data.car.year }}</td>
<td class="text-center">{{ data.car.vin }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container">
<div class="table-header">
<span>التفاصيل المالية</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>الكمية</span>
</th>
<th class="text-center">
<span>سعر الوحدة</span>
</th>
<th class="text-center">
<span>الخصم</span>
</th>
<th class="text-center">
<span>الضريبة</span>
</th>
<th class="text-center">
<span>الإجمالي</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td class="text-center">{{ data.car.marked_price|currency_format}}</td>
<td class="text-center">{{ data.discount_amount|currency_format}}</td>
<td class="text-center">{{ data.vat_amount|currency_format }}</td>
<td class="text-center">{{ data.final_price|currency_format }}</td>
</tr>
</tbody>
</table>
</div>
{% if data.additional_services %}
<div class="table-container">
<div class="table-header">
<span>الخدمات الإضافية</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">النوع</th>
<th class="text-center">القيمة</th>
<th class="text-center">ضريبة الخدمة</th>
<th class="text-center">الإجمالي</th>
</tr>
</thead>
<tbody>
{% for service in data.additional_services.services %}
<tr>
<td class="text-center">{{ service.0.name }}</td>
<td class="text-center">{{ service.0.price|currency_format }}</td>
<td class="text-center">{{ service.1|currency_format}}</td>
<td class="text-center">{{ service.0.price_|currency_format}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td class="text-center"></td>
<td class="text-center">{{ data.total_services_amount|currency_format}}</td>
<td class="text-center">{{ data.total_services_vat|currency_format}}</td>
<td class="text-center">{{ data.total_services_amount_|currency_format}}</td>
</tr>
</tfoot>
</table>
</div>
{% endif %}
<div class="document-details" style="margin-top: 30px; ">
<div class="section text-left">
<p><span class="label">إجمالي ضريبة القيمة المضافة&nbsp;:&nbsp;</span> {{ data.total_vat|currency_format }}</p>
<p><span class="label">الإجمالي الكلي&nbsp;:&nbsp;</span> {{ data.grand_total|currency_format}}</p>
<p><span class="label">كتابةً&nbsp;:&nbsp;</span> {{ data.grand_total|num_to_words }}</p>
</div>
</div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div>
<div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,360 @@
{% load static custom_filters num2words_tags %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Quotation</title>
<style>
/* General Body and Font Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
position: relative;
}
/* Page Layout and Margins for PDF */
@page {
size: A4;
margin: 20mm;
@top-left {
content: "صفحة " counter(page) " من " counter(pages);
font-size: 10px;
color: #555;
}
}
/* Header Styles */
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3;
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
}
/* Document Details Section */
.document-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 80%;
border-collapse: collapse;
margin: 20px auto; /* Centering the table */
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;2px solid #333;
}
/* Responsive and layout helpers */
.table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.table-header span {
font-size: 9px;
font-weight: lighter;
}
/* Footer Styles */
.document-footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
font-size: 10px;
color: #888;
padding-top: 15px;
margin: 0 auto; /* This change centers the footer container */
}
.footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.footer-logo {
text-align: center;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style>
</head>
<body>
<div class="document-header">
<div>
<h1>{{ dealer_info.name }}</h1>
<address>
Address&nbsp;:&nbsp;{{ dealer_info.address }}<br>
Email&nbsp;:&nbsp;{{ dealer_info.user.email }}<br>
Phone&nbsp;:&nbsp;{{ dealer_info.phone_number }}<br>
Commercial Registration No.&nbsp;:&nbsp;{{dealer_info.crn }}<br>
VAT No.&nbsp;:&nbsp;{{ dealer_info.vrn }}
</address>
</div>
<div>
<div class="dealer-logo ">
{% if dealer_info.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer_info.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div>
<h1>Quotation</h1>
<h2 style="font-size: 18px;">{{ estimate.estimate_number }}</h2>
</div>
</div>
<div class="document-details">
<div class="section">
<h2>To: {{ customer_obj.full_name }}</h2>
<p><span class="label">Customer&nbsp;:&nbsp;{{ customer_obj.full_name }}</span></p>
<p><span class="label">Email&nbsp;:&nbsp;{{ customer_obj.email |default:"N/A"}}</span></p>
<p><span class="label">Phone&nbsp;:&nbsp;{{ customer_obj.phone_number|default:"N/A" }}</span></p>
<p><span class="label">Address&nbsp;:&nbsp;{{ customer_obj.address|default:"N/A" }}</span></p>
</div>
<div class="section text-right">
<h2>Details:</h2>
<p><span class="label">Quotation Number&nbsp;:&nbsp;</span> {{ estimate.estimate_number }}</p>
<p><span class="label">Issue Date&nbsp;:&nbsp;</span> {{ estimate.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">Payment Method&nbsp;:&nbsp;</span> {{ estimate.get_terms_display }}</p>
</div>
</div>
<div class="table-container">
<div class="table-header">
<span>Car Details</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>Make</span>
</th>
<th class="text-center">
<span>Model</span>
</th>
<th class="text-center">
<span>Series</span>
</th>
<th class="text-center">
<span>Trim</span>
</th>
<th class="text-center">
<span>Year</span>
</th>
<th class="text-center">
<span>VIN</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">{{ data.car.id_car_make.name }}</td>
<td class="text-center">{{ data.car.id_car_model.name }}</td>
<td class="text-center">{{ data.car.id_car_serie.name }}</td>
<td class="text-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center">{{ data.car.year }}</td>
<td class="text-center">{{ data.car.vin }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container">
<div class="table-header">
<span>Financial Details</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>Quantity</span>
</th>
<th class="text-center">
<span>Unit Price</span>
</th>
<th class="text-center">
<span>Discount</span>
</th>
<th class="text-center">
<span>VAT</span>
</th>
<th class="text-center">
<span>Total</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td class="text-center">{{ data.car.marked_price|currency_format}}</td>
<td class="text-center">{{ data.discount_amount|currency_format }}</td>
<td class="text-center">{{ data.vat_amount|currency_format}}</td>
<td class="text-center">{{ data.final_price|currency_format}}</td>
</tr>
</tbody>
</table>
</div>
{% if data.additional_services %}
<div class="table-container">
<div class="table-header">
<span>Additional Services</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">Type</th>
<th class="text-center">Value</th>
<th class="text-center">Service VAT</th>
<th class="text-center">Total</th>
</tr>
</thead>
<tbody>
{% for service in data.additional_services.services %}
<tr>
<td class="text-center">{{ service.0.name }}</td>
<td class="text-center">{{ service.0.price|currency_format }}</td>
<td class="text-center">{{ service.1|currency_format}}</td>
<td class="text-center">{{ service.0.price_|currency_format}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td class="text-center"></td>
<td class="text-center">{{ data.total_services_amount|currency_format}}</td>
<td class="text-center">{{ data.total_services_vat|currency_format}}</td>
<td class="text-center">{{ data.total_services_amount_|currency_format }}</td>
</tr>
</tfoot>
</table>
</div>
{% endif %}
<div class="document-details" style="margin-top: 30px; ">
<div class="section text-right">
<p><span class="label">Total VAT&nbsp;:&nbsp;</span> {{ data.total_vat|currency_format}}</p>
<p><span class="label">Grand Total&nbsp;:&nbsp;</span> {{ data.grand_total|currency_format }}</p>
<p><span class="label">In words&nbsp;:&nbsp;</span> {{ data.grand_total|num_to_words }}</p>
</div>
</div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div>
<div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div>
</div>
</body>
</html>

View File

@ -28,7 +28,7 @@
<i class="fa-solid fa-floppy-disk me-1"></i> <i class="fa-solid fa-floppy-disk me-1"></i>
{% trans 'Save' %} {% trans 'Save' %}
</button> </button>
<a href="{% url 'home' request.dealer.slug %}" <a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
class="btn btn-phoenix-secondary btn-lg"> class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i> <i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %} {% trans "Cancel" %}
@ -40,3 +40,4 @@
</div> </div>
</main> </main>
{% endblock content %} {% endblock content %}

View File

@ -143,7 +143,17 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}" <a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}"
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block"><i class="fa-regular fa-eye"></i> {% trans 'Preview' %}</span></a> class="btn btn-phoenix-primary" target="_blank" rel="noopener noreferrer"><span class="d-none d-sm-inline-block" ><i class="fa-regular fa-eye"></i> {% trans 'Preview' %}</span></a>
<a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}?lang=en"
class="btn btn-phoenix-secondary"
target="_blank">
<span class="d-none d-sm-inline-block"><i class="fas fa-print me-2"></i>{% trans 'Print EN' %}</span>
</a>
<a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}?lang=ar"
class="btn btn-phoenix-secondary"
target="_blank">
<span class="d-none d-sm-inline-block"><i class="fas fa-print me-2"></i>{% trans 'Print AR' %}</span>
</a>
</div> </div>
</div> </div>
{{ invoice.amount_owned }} {{ invoice.amount_owned }}
@ -369,7 +379,7 @@
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>

View File

@ -4,69 +4,80 @@
{{ _("Invoices") }} {{ _("Invoices") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="container-fluid py-5">
{% if invoices or request.GET.q %} {% if invoices or request.GET.q %}
<div class="row mt-4"> <div class="card shadow-lg border-0 rounded-4">
<div class="row g-3 justify-content-between mb-4"> <div class="card-body p-4 p-md-5">
<div class="col-auto">
<div class="d-md-flex justify-content-between"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
<h2 class="mb-3"> <div class="mb-3 mb-md-0">
{% trans "Invoices" %}<i class="fa-solid fa-receipt ms-2 text-primary"></i> <h1 class="display-5 fw-bolder mb-0">
</h2> {% trans "Invoices" %}
<i class="fa-solid fa-receipt ms-2 text-primary"></i>
</h1>
<p class="text-secondary mt-1 mb-0">
{% trans "Manage and track all your customer invoices." %}
</p>
</div> </div>
</div> </div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div> {% comment %}
<div class="d-flex justify-content-end mb-4">
{% include 'partials/search_box.html' %}
</div> </div>
</div> {% endcomment %}
<div class="table-responsive px-1 scrollbar">
<table class="table align-items-center table-flush"> <div class="table-responsive">
<thead> <table class="table table-hover align-middle">
<tr class="bg-body-highlight"> <thead class="bg-light">
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Invoice Number" %}</th> <tr class="text-uppercase text-secondary fw-bold small">
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Customer" %}</th> <th scope="col" style="width:20%">{% trans "Invoice Number" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Status" %}</th> <th scope="col" style="width:20%">{% trans "Customer" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Status Date" %}</th> <th scope="col" style="width:15%">{% trans "Status" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Created" %}</th> <th scope="col" style="width:20%">{% trans "Status Date" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Actions" %}</th> <th scope="col" style="width:15%">{% trans "Created" %}</th>
<th scope="col" style="width:10%" class="text-end">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="list"> <tbody>
{% for invoice in invoices %} {% for invoice in invoices %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-shadow">
<td class="align-middle product white-space-nowrap py-0 px-1">{{ invoice.invoice_number }}</td> <td class="align-middle fw-semibold ">{{ invoice.invoice_number }}</td>
<td class="align-middle product white-space-nowrap">{{ invoice.customer }}</td> <td class="align-middle text-body-tertiary">{{ invoice.customer }}</td>
<td class="align-middle product white-space-nowrap text-success"> <td class="align-middle">
{% if invoice.is_past_due %} {% if invoice.is_past_due %}
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Past Due" %}</span> <span class="badge rounded-pill text-bg-danger">{% trans "Past Due" %}</span>
{% elif invoice.is_approved %} {% elif invoice.is_approved %}
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span> <span class="badge rounded-pill text-bg-success">{% trans "Approved" %}</span>
{% elif invoice.is_canceled %} {% elif invoice.is_canceled %}
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Canceled" %}</span> <span class="badge rounded-pill text-bg-secondary">{% trans "Canceled" %}</span>
{% elif invoice.is_draft %} {% elif invoice.is_draft %}
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span> <span class="badge rounded-pill text-bg-warning">{% trans "Draft" %}</span>
{% elif invoice.is_review %} {% elif invoice.is_review %}
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span> <span class="badge rounded-pill text-bg-info">{% trans "In Review" %}</span>
{% elif invoice.is_paid %} {% elif invoice.is_paid %}
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Paid" %}</span> <span class="badge rounded-pill text-bg-success">{% trans "Paid" %}</span>
{% endif %} {% endif %}
</td> </td>
<td class="align-middle product white-space-nowrap"> <td class="align-middle text-body-tertiary">
{% if invoice.invoice_status == "in_review" %} {% if invoice.invoice_status == "in_review" %}
{{ invoice.date_in_review }} {{ invoice.date_in_review|date:"M d, Y"|default:"N/A" }}
{% elif invoice.invoice_status == "approved" %} {% elif invoice.invoice_status == "approved" %}
{{ invoice.date_approved }} {{ invoice.date_approved|date:"M d, Y"|default:"N/A" }}
{% elif invoice.invoice_status == "canceled" %} {% elif invoice.invoice_status == "canceled" %}
{{ invoice.date_canceled }} {{ invoice.date_canceled|date:"M d, Y"|default:"N/A" }}
{% elif invoice.invoice_status == "draft" %} {% elif invoice.invoice_status == "draft" %}
{{ invoice.date_draft }} {{ invoice.date_draft|date:"M d, Y"|default:"N/A" }}
{% elif invoice.invoice_status == "paid" %} {% elif invoice.invoice_status == "paid" %}
{{ invoice.date_paid }} {{ invoice.date_paid|date:"M d, Y"|default:"N/A" }}
{% endif %} {% endif %}
</td> </td>
<td class="align-middle product white-space-nowrap">{{ invoice.created }}</td> <td class="align-middle text-body-tertiary">
<td class="align-middle product white-space-nowrap"> {{ invoice.created|date:"M d, Y" }}
</td>
<td class="align-middle text-end">
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
class="btn btn-sm btn-phoenix-success"> class="btn btn-sm btn-primary">
<i class="fa-regular fa-eye me-1"></i> <i class="fa-regular fa-eye me-1"></i>
{% trans "View" %} {% trans "View" %}
</a> </a>
@ -74,20 +85,24 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="6" class="text-center">{% trans "No Invoice Found" %}</td> <td colspan="6" class="text-center py-5 text-muted">{% trans "No invoices found." %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% if page_obj.paginator.num_pages > 1 %} {% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-4">
<div class="d-flex">{% include 'partials/pagination.html' %}</div> {% include 'partials/pagination.html' %}
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
{% else %} {% else %}
{% url 'estimate_create' request.dealer.slug as url %} {% url 'estimate_create' request.dealer.slug as create_url %}
{% include "empty-illustration-page.html" with value=_("invoice") url=url %} {% include "empty-illustration-page.html" with title=_("No Invoices Yet") subtitle=_("Looks like you haven't created any invoices. Start by creating a new invoice or quotation.") url=create_url button_text=_("Create First Invoice") %}
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -1,367 +1,333 @@
{% load i18n static custom_filters num2words_tags %} {% load i18n static custom_filters num2words_tags %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ar" dir="rtl"> <html lang="ar">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% trans "Invoice Preview" %}</title>
<title>{% trans "Invoice" %}</title>
<link href="{% static 'css/theme.min.css' %}"
type="text/css"
rel="stylesheet"
id="style-default">
<link href="{% static 'css/user.min.css' %}"
type="text/css"
rel="stylesheet"
id="user-style-default">
<link href="{% static 'css/custom.css' %}" type="text/css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
rel="stylesheet">
<style> <style>
/* General Body and Font Styles */
body { body {
font-family: 'Roboto', sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
}
.invoice-container {
width: 210mm;
min-height: 297mm;
padding: 10mm;
margin: 10mm auto;
background: white;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.invoice-content {
flex-grow: 1;
}
.invoice-header {
text-align: center;
border-bottom: 2px solid #dee2e6;
padding-bottom: 10px;
margin-bottom: 20px;
}
.qr-code {
text-align: center;
margin-top: 10px;
}
.qr-code img {
width: 3cm;
height: 3cm;
border-radius: 0.3333333333rem;
}
.invoice-details, .invoice-table {
font-size: 14px; font-size: 14px;
color: #333;
background-color: #f7f7f7;
margin: 0;
padding: 20px;
} }
.invoice-table th { /* Container for the document content */
background-color: #f8f9fa; .document-container {
font-weight: 600; max-width: 800px;
margin: 0 auto;
background-color: #fff;
padding: 40px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
} }
.invoice-total { /* Header Styles */
text-align: right; .document-header {
font-size: 16px;
font-weight: 600;
margin-top: 10px;
}
.footer-note {
margin-top: auto;
padding-top: 10mm;
font-size: 13px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-top: 2px solid #dee2e6; /* Add a top border to separate from content */ border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3;
}
.document-header h2 {
font-size: 18px;
margin: 0;
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
} }
.logo-img img { /* Document Details Section */
width: 10mm; .document-details {
height: 10mm; display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 12px;
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;
}
/* Responsive and layout helpers */
.table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.table-header span {
font-size: 14px;
font-weight: normal;
}
/* Footer Styles */
.document-footer {
text-align: center;
font-size: 10px;
color: #888;
border-top: 1px solid #ddd;
padding-top: 15px;
margin-top: 30px;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
margin: 5px 0 0;
}
.footer-powered p {
font-size: 11px;
margin: 0;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="row p-2"> <div class="document-container">
<div class="col-2"> <div class="document-header">
<button class="btn btn-sm btn-phoenix-danger w-100" <div>
onclick="window.history.back()">الرجوع&nbsp;/&nbsp;Back</button> <h1>{{ dealer_info.name }}</h1>
<address>
العنوان&nbsp;:&nbsp;{{ dealer_info.address }}<br>
البريد الإلكتروني&nbsp;:&nbsp;{{ dealer_info.user.email }}<br>
الهاتف&nbsp;:&nbsp;{{ dealer_info.phone_number }}<br>
رقم السجل التجاري&nbsp;:&nbsp;{{dealer_info.crn }}&nbsp;&nbsp;|&nbsp;الرقم الضريبي&nbsp;:&nbsp;{{ dealer_info.vrn }}
</address>
</div> </div>
<div class="col-2"> <div>
<button class="btn btn-sm btn-phoenix-primary w-100" id="download-pdf">تحميل&nbsp;/&nbsp;Download</button> <h1>عرض سعر</h1>
</div> <h2>{{ estimate.estimate_number }}</h2>
<div class="col-8"></div>
</div>
<div class="invoice-container" id="invoice-content">
<div class="invoice-content">
<div class="invoice-header">
<h5 class="fs-5">
<span>Invoice</span>&nbsp;/&nbsp;<span>فاتورة</span>
</h5>
</div>
<div class="invoice-details p-1">
<div class="d-flex justify-content-around align-items-end">
<div class="d-flex justify-content-start align-items-center">
<div class="qr-code">
<img src="{% static 'qr_code/Marwan_qr.png' %}" alt="QR Code">
</div> </div>
</div> </div>
<div class="dealer-logo ">
{% if request.dealer.logo %} <div class="document-details">
<img class="rounded-soft" <div class="section">
style="max-width:150px; <h2>{{ estimate.customer.customer_name }}: إلى</h2>
max-height:150px" <p><span class="label">العميل&nbsp;:&nbsp;{{ customer_obj.full_name }}</span></p>
src="{{ request.dealer.logo.url|default:'' }}" <p><span class="label">البريد الإلكتروني&nbsp;:&nbsp;{{ customer_obj.email |default:"N/A"}}</span></p>
alt="Dealer Logo" /> <p><span class="label">الهاتف&nbsp;:&nbsp;{{ customer_obj.phone_number|default:"N/A" }}</span></p>
{% endif %} <p><span class="label">العنوان&nbsp;:&nbsp;{{ customer_obj.address|default:"N/A" }}</span></p>
</div>
<div class="section text-left">
<h2>التفاصيل:</h2>
<p><span class="label">رقم عرض السعر&nbsp;:&nbsp;</span> {{ estimate.estimate_number }}</p>
<p><span class="label">تاريخ الإصدار&nbsp;:&nbsp;</span> {{ estimate.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">طريقة الدفع&nbsp;:&nbsp;</span> {{ estimate.get_terms_display }}</p>
</div> </div>
</div> </div>
<table class="table table-sm table-responsive border-gray-50">
<tr> <div class="table-container">
<td class="ps-1"> <div class="table-header">
<strong>Dealership Name</strong> <span>تفاصيل السيارة</span>
</td>
<td class="text-center">{{ request.dealer.name }}</td>
<td class="text-end">
<strong>اسم الوكالة</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Dealership Address</strong>
</td>
<td class="text-center">{{ request.dealer.address }}</td>
<td class="text-end">
<strong>عنوان الوكالة</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Phone</strong>
</td>
<td class="text-center">{{ request.dealer.phone_number }}</td>
<td class="text-end">
<strong>جوال</strong>
</td>
</tr>
<tr>
<td>
<strong>VAT Number</strong>
</td>
<td class="text-center">{{ request.dealer.vrn }}</td>
<td class="text-end">
<strong>الرقم الضريبي</strong>
</td>
</tr>
</table>
<table class="table table-sm table-bordered border-gray-50 ">
<tr>
<td class="ps-1">
<strong>Invoice&nbsp;Number</strong>
</td>
<td class="text-center">{{ invoice.invoice_number }}</td>
<td class="text-end p-1">
<strong>رقم الفاتورة</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Date</strong>
</td>
<td class="text-center">{{ invoice.date_approved| date:"Y/m/d" }}</td>
<td class="text-end p-1">
<strong>التاريخ</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Customer Name</strong>
</td>
<td class="text-center">{{ invoice.customer.customer_name }}</td>
<td class="text-end p-1">
<strong>اسم العميل</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Email</strong>
</td>
<td class="text-center">{{ invoice.customer.email |default:"N/A" }}</td>
<td class="text-end p-1">
<strong>البريد الإلكتروني</strong>
</td>
</tr>
<tr>
<td class="ps-1">
<strong>Terms</strong>
</td>
<td class="text-center">{{ invoice.get_terms_display }}</td>
<td class="text-end p-1">
<strong>شروط الدفع</strong>
</td>
</tr>
</table>
</div> </div>
<div class="d-flex justify-content-between"> <table class="table">
<span class="fs-9 fw-thin">Car Details</span>
<span class="fs-9 fw-thin">تفاصيل السيارة</span>
</div>
<div class="invoice-table p-1">
<table class="table table-sm table-bordered m-1">
<thead> <thead>
<tr> <tr>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">Make</span> / <span class="fs-10">الصانع</span> <span>الصانع</span>
</th> </th>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">Model</span> / <span class="fs-10">الموديل</span> <span>الموديل</span>
</th> </th>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">Series</span> / <span class="fs-10">السلسلة</span> <span>السلسلة</span>
</th> </th>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">Trim</span> / <span class="fs-10">الفئة</span> <span>الفئة</span>
</th> </th>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">Year</span> / <span class="fs-10">السنة</span> <span>السنة</span>
</th> </th>
<th class="text-wrap text-center align-content-center"> <th class="text-center">
<span class="fs-10">VIN</span> / <span class="fs-10">رقم الهيكل</span> <span>رقم الهيكل</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">Quantity</span> / <span class="fs-10">الكمية</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">Unit Price</span> / <span class="fs-10">سعر الوحدة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">Discount</span> / <span class="fs-10">الخصم</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">VAT</span> / <span class="fs-10">الضريبة</span>
</th>
<th class="text-wrap text-center align-content-center">
<span class="fs-10">Total</span> / <span class="fs-10">الإجمالي</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_make.name }}</td> <td class="text-center">{{ data.car.id_car_make.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_model.name }}</td> <td class="text-center">{{ data.car.id_car_model.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_serie.name }}</td> <td class="text-center">{{ data.car.id_car_serie.name }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.id_car_trim.name }}</td> <td class="text-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center fs-10 align-content-center">{{ data.car.year }}</td> <td class="text-center">{{ data.car.year }}</td>
<td class="ps-1 fs-10 align-content-center">{{ data.car.vin }}</td> <td class="text-center">{{ data.car.vin }}</td>
<td class="text-center fs-10 align-content-center">1</td>
<td class="text-center fs-10 align-content-center">{{ data.car.marked_price |floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.discount_amount |floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.vat_amount|floatformat:2 }}</td>
<td class="text-center fs-10 align-content-center">{{ data.final_price|floatformat:2 }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex justify-content-between">
<span class="fs-9 fw-thin">Additional&nbsp;Services</span> <div class="table-container">
<span class="fs-9 fw-thin">الخدمات&nbsp;الإضافية</span> <div class="table-header">
<span>التفاصيل المالية</span>
</div> </div>
{% if data.additional_services %} <table class="table">
<div class="invoice-table p-1">
<table class="table table-sm table-bordered m-1">
<thead> <thead>
<tr> <tr>
<th class="text-center fs-10 align-content-center">Type&nbsp;/&nbsp;النوع</th> <th class="text-center">
<th class="text-center fs-10 align-content-center">Price&nbsp;/&nbsp;السعر</th> <span>الكمية</span>
<th class="text-center fs-10 align-content-center">Service VAT&nbsp;/&nbsp;ضريبة الخدمة</th>
<th class="text-center fs-10 align-content-center">
<span class="fs-10">Total</span> / <span class="fs-10">الإجمالي</span>
</th> </th>
<th class="text-center">
<span>سعر الوحدة</span>
</th>
<th class="text-center">
<span>الخصم</span>
</th>
<th class="text-center">
<span>الضريبة</span>
</th>
<th class="text-center">
<span>الإجمالي</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td class="text-center">{{ data.car.marked_price|floatformat:2 }}</td>
<td class="text-center">{{ data.discount_amount|floatformat:2 }}</td>
<td class="text-center">{{ data.vat_amount|floatformat:2 }}</td>
<td class="text-center">{{ data.final_price|floatformat:2 }}</td>
</tr>
</tbody>
</table>
</div>
{% if data.additional_services %}
<div class="table-container">
<div class="table-header">
<span>الخدمات الإضافية</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">النوع</th>
<th class="text-center">القيمة</th>
<th class="text-center">ضريبة الخدمة</th>
<th class="text-center">الإجمالي</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for service in data.additional_services.services %} {% for service in data.additional_services.services %}
<tr> <tr>
<td class="ps-1 text-start fs-10 align-content-center">{{ service.0.name }}</td> <td class="text-center">{{ service.0.name }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.0.price|floatformat }}</td> <td class="text-center">{{ service.0.price|floatformat:2 }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.1|floatformat }}</td> <td class="text-center">{{ service.1|floatformat:2 }}</td>
<td class="ps-1 text-center fs-10 align-content-center">{{ service.0.price_|floatformat }}</td> <td class="text-center">{{ service.0.price_|floatformat:2 }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot>
<tr>
<td class="text-center"></td>
<td class="text-center">{{ data.total_services_amount|floatformat:'2g'}}</td>
<td class="text-center">{{ data.total_services_vat|floatformat:'2g'}}</td>
<td class="text-center">{{ data.total_services_amount_|floatformat:'2g' }}</td>
</tr>
</tfoot>
</table> </table>
</div> </div>
{% endif %} {% endif %}
<div class="invoice-total d-flex justify-content-end">
<div class="table-responsive"> <div class="document-details" style="margin-top: 30px; ">
<table class="table table-sm table-responsive"> <div class="section text-left">
<tr> <p><span class="label">إجمالي ضريبة القيمة المضافة&nbsp;:&nbsp;</span> {{ data.total_vat|floatformat:'2g' }}</p>
<td class="text-start ps-1"> <p><span class="label">الإجمالي الكلي&nbsp;:&nbsp;</span> {{ data.grand_total|floatformat:'2g' }}</p>
<strong class="fs-9">Total VAT</strong> <p><span class="label">كتابةً&nbsp;:&nbsp;</span> {{ data.grand_total|num_to_words }}</p>
</td>
<td class="text-center">
<span class="fs-9">{{ data.total_vat|floatformat }} <span class="icon-saudi_riyal"></span></span>
</td>
<td class="text-end">
<strong class="fs-9">إجمالي&nbsp;ضريبة&nbsp;القيمة&nbsp;المضافة</strong>
</td>
</tr>
<tr>
<td class="text-start ps-1">
<strong class="fs-9">Grand Total</strong>
</td>
<td class="text-center">
<span class="fs-9">{{ data.grand_total|floatformat }}&nbsp;<span class="icon-saudi_riyal"></span></span>
</td>
<td class="text-end">
<strong class="fs-9">الإجمالي&nbsp;الكلي</strong>
</td>
</tr>
<tr>
<td class="text-end" colspan="3">
<span class="fs-9 fw-bold">كتابةً:&nbsp;</span><span class="fs-9">{{ data.grand_total|num_to_words }}&nbsp;<span class="icon-saudi_riyal"></span></span>
</td>
</tr>
</table>
</div> </div>
</div> </div>
</div> <hr style="border-bottom:1px solid #ccc; ">
<div class="footer-note">
<div class="logo-img text-center"> <div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" /> <img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p class="fs-9 fw-bold"> <p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span> <span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p> </p>
</div> </div>
<p class="fs-11"> <div class="footer-powered">
<span class="fw-thin">Powered&nbsp;by&nbsp;</span> <p>
<a class="text-decoration-none fs-9" <span>Powered&nbsp;by&nbsp;</span>
href="https://tenhal.sa" <a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
style="color: #112e40"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p> </p>
</div> </div>
</div> </div>
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> </div>
<script src="{% static 'js/html2pdf.bundle.min.js' %}"></script>
<script>
document.getElementById('download-pdf').addEventListener('click', function () {
html2pdf().from(document.getElementById('invoice-content')).set({
margin: 0,
filename: "{{ invoice.invoice_number }}_{{ invoice.customer.customer_name  }}_{{invoice.date_approved|date:'Y-m-d' }}.pdf",
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 3 },
jsPDF: { unit: 'mm', format: 'a3', orientation: 'portrait' }
}).save();
});
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,359 @@
{% load static custom_filters num2words_tags %}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>فاتورة</title>
<style>
/* General Body and Font Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
position: relative;
}
/* Page Layout and Margins for PDF */
@page {
size: A4;
margin: 20mm;
@top-left {
content: "صفحة " counter(page) " من " counter(pages);
font-size: 10px;
color: #555;
}
}
/* Header Styles */
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3;
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
}
/* Document Details Section */
.document-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 80%;
border-collapse: collapse;
margin: 20px auto; /* Centering the table */
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;2px solid #333;
}
/* Responsive and layout helpers */
.table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.table-header span {
font-size: 9px;
font-weight: lighter;
}
/* Footer Styles */
.document-footer {
position: relative;
bottom: 0;
left: 40px;
width: 100%;
text-align: center;
font-size: 10px;
color: #888;
padding-top: 15px;
margin: 0 20mm;
}
.footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.footer-logo {
text-align: center;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style>
</head>
<body>
<div class="document-header">
<div>
<h1>{{ dealer_info.name }}</h1>
<address>
العنوان&nbsp;:&nbsp;{{ dealer_info.address }}<br>
البريد الإلكتروني&nbsp;:&nbsp;{{ dealer_info.user.email }}<br>
الهاتف&nbsp;:&nbsp;{{ dealer_info.phone_number }}<br>
رقم السجل التجاري&nbsp;:&nbsp;{{dealer_info.crn }}<br>
الرقم الضريبي&nbsp;:&nbsp;{{ dealer_info.vrn }}
</address>
</div>
<div>
<div class="dealer-logo ">
{% if dealer_info.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer_info.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div>
<h1>فاتورة</h1>
<h2 style="font-size: 18px;">{{ invoice.invoice_number }}</h2>
</div>
</div>
<div class="document-details">
<div class="section">
<h2>{{ customer_obj.full_name }}: إلى</h2>
<p><span class="label">العميل&nbsp;:&nbsp;{{ customer_obj.full_name }}</span></p>
<p><span class="label">البريد الإلكتروني&nbsp;:&nbsp;{{ customer_obj.email |default:"N/A"}}</span></p>
<p><span class="label">الهاتف&nbsp;:&nbsp;{{ customer_obj.phone_number|default:"N/A" }}</span></p>
<p><span class="label">العنوان&nbsp;:&nbsp;{{ customer_obj.address|default:"N/A" }}</span></p>
</div>
<div class="section text-left">
<h2>التفاصيل:</h2>
<p><span class="label">رقم عرض السعر&nbsp;:&nbsp;</span> {{ invoice.invoice_number }}</p>
<p><span class="label">تاريخ الإصدار&nbsp;:&nbsp;</span> {{ invoice.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">طريقة الدفع&nbsp;:&nbsp;</span> {{ invoice.get_terms_display }}</p>
</div>
</div>
<div class="table-container">
<div class="table-header">
<span>تفاصيل السيارة</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>الصانع</span>
</th>
<th class="text-center">
<span>الموديل</span>
</th>
<th class="text-center">
<span>السلسلة</span>
</th>
<th class="text-center">
<span>الفئة</span>
</th>
<th class="text-center">
<span>السنة</span>
</th>
<th class="text-center">
<span>رقم الهيكل</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">{{ data.car.id_car_make.name }}</td>
<td class="text-center">{{ data.car.id_car_model.name }}</td>
<td class="text-center">{{ data.car.id_car_serie.name }}</td>
<td class="text-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center">{{ data.car.year }}</td>
<td class="text-center">{{ data.car.vin }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container">
<div class="table-header">
<span>التفاصيل المالية</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>الكمية</span>
</th>
<th class="text-center">
<span>سعر الوحدة</span>
</th>
<th class="text-center">
<span>الخصم</span>
</th>
<th class="text-center">
<span>الضريبة</span>
</th>
<th class="text-center">
<span>الإجمالي</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td class="text-center">{{ data.car.marked_price|currency_format }}</td>
<td class="text-center">{{ data.discount_amount|currency_format}}</td>
<td class="text-center">{{ data.vat_amount|currency_format}}</td>
<td class="text-center">{{ data.final_price|currency_format }}</td>
</tr>
</tbody>
</table>
</div>
{% if data.additional_services %}
<div class="table-container">
<div class="table-header">
<span>الخدمات الإضافية</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">النوع</th>
<th class="text-center">القيمة</th>
<th class="text-center">ضريبة الخدمة</th>
<th class="text-center">الإجمالي</th>
</tr>
</thead>
<tbody>
{% for service in data.additional_services.services %}
<tr>
<td class="text-center">{{ service.0.name }}</td>
<td class="text-center">{{ service.0.price|currency_format}}</td>
<td class="text-center">{{ service.1|currency_format }}</td>
<td class="text-center">{{ service.0.price_|currency_format}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td class="text-center"></td>
<td class="text-center">{{ data.total_services_amount|currency_format}}</td>
<td class="text-center">{{ data.total_services_vat|currency_format}}</td>
<td class="text-center">{{ data.total_services_amount_|currency_format }}</td>
</tr>
</tfoot>
</table>
</div>
{% endif %}
<div class="document-details" style="margin-top: 30px; ">
<div class="section text-left">
<p><span class="label">إجمالي ضريبة القيمة المضافة&nbsp;:&nbsp;</span> {{ data.total_vat|currency_format }}</p>
<p><span class="label">الإجمالي الكلي&nbsp;:&nbsp;</span> {{ data.grand_total|currency_format}}</p>
<p><span class="label">كتابةً&nbsp;:&nbsp;</span> {{ data.grand_total|num_to_words }}</p>
</div>
</div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div>
<div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,361 @@
{% load static custom_filters num2words_tags %}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Invoice</title>
<style>
/* General Body and Font Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
position: relative;
}
/* Page Layout and Margins for PDF */
@page {
size: A4;
margin: 20mm;
@top-left {
content: "صفحة " counter(page) " من " counter(pages);
font-size: 10px;
color: #555;
}
}
/* Header Styles */
.document-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #333;
padding-bottom: 15px;
margin-bottom: 20px;
}
.document-header .logo {
max-width: 150px;
height: auto;
}
.document-header h1 {
font-size: 24px;
margin: 0;
color: #0056b3;
}
.document-header address {
text-align: right;
font-style: normal;
font-size: 10px;
}
/* Document Details Section */
.document-details {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
line-height: 1.6;
}
.document-details .section {
width: 48%;
}
.document-details h2 {
font-size: 14px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #555;
}
.document-details p {
margin: 0;
font-size: 12px;
}
.document-details .label {
font-weight: bold;
color: #555;
}
/* Table Styles */
.table {
width: 80%;
border-collapse: collapse;
margin: 20px auto; /* Centering the table */
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: right;
}
.table th {
background-color: #f2f2f2;
font-weight: bold;
}
.table tfoot td {
border-top: 2px solid #333;
font-weight: bold;
}
.text-right {
text-align: left;
}
.text-left {
text-align: right;
}
.text-center {
text-align: center;2px solid #333;
}
/* Responsive and layout helpers */
.table-container {
width: 100%;
overflow-x: auto;
margin-bottom: 20px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.table-header span {
font-size: 9px;
font-weight: lighter;
}
/* Footer Styles */
.document-footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
font-size: 10px;
color: #888;
padding-top: 15px;
margin: 0 auto; /* This change centers the footer container */
}
.footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.footer-logo {
text-align: center;
}
.footer-logo img {
height: 20px;
width: 20px;
}
.footer-logo p {
font-size: 9px;
font-weight: bold;
}
.footer-powered p {
font-size: 11px;
}
.footer-powered span {
font-weight: lighter;
}
.footer-powered a {
color: #112e40;
text-decoration: none;
font-size: 9px;
}
</style>
</head>
<body>
<div class="document-header">
<div>
<h1>{{ dealer_info.name }}</h1>
<address>
Address&nbsp;:&nbsp;{{ dealer_info.address }}<br>
Email&nbsp;:&nbsp;{{ dealer_info.user.email }}<br>
Phone&nbsp;:&nbsp;{{ dealer_info.phone_number }}<br>
Commercial Registration No.&nbsp;:&nbsp;{{dealer_info.crn }}<br>
VAT No.&nbsp;:&nbsp;{{ dealer_info.vrn }}
</address>
</div>
<div>
<div class="dealer-logo ">
{% if dealer_info.logo %}
<img class="rounded-soft"
style="max-width:100px;
max-height:100px"
src="{{dealer_info.logo.url|default:'' }}"
alt="Dealer Logo" />
{% endif %}
</div>
</div>
<div>
<h1>Invoice</h1>
<h2 style="font-size: 18px;">{{ invoice.invoice_number }}</h2>
</div>
</div>
<div class="document-details">
<div class="section">
<h2>To: {{ customer_obj.full_name }}</h2>
<p><span class="label">Customer&nbsp;:&nbsp;{{ customer_obj.full_name }}</span></p>
<p><span class="label">Email&nbsp;:&nbsp;{{ customer_obj.email |default:"N/A"}}</span></p>
<p><span class="label">Phone&nbsp;:&nbsp;{{ customer_obj.phone_number|default:"N/A" }}</span></p>
<p><span class="label">Address&nbsp;:&nbsp;{{ customer_obj.address|default:"N/A" }}</span></p>
</div>
<div class="section text-right">
<h2>Details:</h2>
<p><span class="label">Quotation Number&nbsp;:&nbsp;</span> {{ invoice.invoice_number }}</p>
<p><span class="label">Issue Date&nbsp;:&nbsp;</span> {{ invoice.date_approved|date:"Y/m/d" }}</p>
<p><span class="label">Payment Method&nbsp;:&nbsp;</span> {{ invoiceget_terms_display }}</p>
</div>
</div>
<div class="table-container">
<div class="table-header">
<span>Car Details</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>Make</span>
</th>
<th class="text-center">
<span>Model</span>
</th>
<th class="text-center">
<span>Series</span>
</th>
<th class="text-center">
<span>Trim</span>
</th>
<th class="text-center">
<span>Year</span>
</th>
<th class="text-center">
<span>VIN</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">{{ data.car.id_car_make.name }}</td>
<td class="text-center">{{ data.car.id_car_model.name }}</td>
<td class="text-center">{{ data.car.id_car_serie.name }}</td>
<td class="text-center">{{ data.car.id_car_trim.name }}</td>
<td class="text-center">{{ data.car.year }}</td>
<td class="text-center">{{ data.car.vin }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container">
<div class="table-header">
<span>Financial Details</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">
<span>Quantity</span>
</th>
<th class="text-center">
<span>Unit Price</span>
</th>
<th class="text-center">
<span>Discount</span>
</th>
<th class="text-center">
<span>VAT</span>
</th>
<th class="text-center">
<span>Total</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td class="text-center">{{ data.car.marked_price|currency_format}}</td>
<td class="text-center">{{ data.discount_amount|currency_format }}</td>
<td class="text-center">{{ data.vat_amount|currency_format}}</td>
<td class="text-center">{{ data.final_price|currency_format }}</td>
</tr>
</tbody>
</table>
</div>
{% if data.additional_services %}
<div class="table-container">
<div class="table-header">
<span>Additional Services</span>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center">Type</th>
<th class="text-center">Value</th>
<th class="text-center">Service VAT</th>
<th class="text-center">Total</th>
</tr>
</thead>
<tbody>
{% for service in data.additional_services.services %}
<tr>
<td class="text-center">{{ service.0.name }}</td>
<td class="text-center">{{ service.0.price|currency_format }}</td>
<td class="text-center">{{ service.1|currency_format}}</td>
<td class="text-center">{{ service.0.price_|currency_format}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td class="text-center"></td>
<td class="text-center">{{ data.total_services_amount|currency_format}}</td>
<td class="text-center">{{ data.total_services_vat|currency_format}}</td>
<td class="text-center">{{ data.total_services_amount_|currency_format }}</td>
</tr>
</tfoot>
</table>
</div>
{% endif %}
<div class="document-details" style="margin-top: 30px; ">
<div class="section text-right">
<p><span class="label">Total VAT&nbsp;:&nbsp;</span> {{ data.total_vat|currency_format}}</p>
<p><span class="label">Grand Total&nbsp;:&nbsp;</span> {{ data.grand_total|currency_format}}</p>
<p><span class="label">In words&nbsp;:&nbsp;</span> {{ data.grand_total|num_to_words }}</p>
</div>
</div>
<hr style="border-bottom:1px solid #ccc; ">
<div class="document-footer">
<div class="footer-logo">
<img src="{% static 'images/logos/logo-d-pdf.png' %}" alt="Logo" />
<p>
<span>Haikal</span>&nbsp;|&nbsp;<span>هيكل</span>
</p>
</div>
<div class="footer-powered">
<p>
<span>Powered&nbsp;by&nbsp;</span>
<a href="https://tenhal.sa"><span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span></a>
</p>
</div>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More