everything before new pull

This commit is contained in:
Faheed 2025-09-24 13:14:56 +03:00
commit 043d885ece
155 changed files with 8664 additions and 8084 deletions

View File

@ -34,9 +34,7 @@ application = ProtocolTypeRouter(
URLRouter( URLRouter(
[ [
path("sse/notifications/", NotificationSSEApp()), path("sse/notifications/", NotificationSSEApp()),
re_path( re_path(r"", app),
r"", app
),
] ]
) )
), ),

View File

@ -36,4 +36,3 @@ urlpatterns += i18n_patterns(
if 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) 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,13 +2266,21 @@ 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(

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.

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

@ -57,6 +57,7 @@ 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
@ -66,6 +67,7 @@ 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)
@ -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"),
@ -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,7 +3936,9 @@ 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)
@ -3900,7 +3946,14 @@ class UserRegistration(models.Model):
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

@ -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):
@ -998,17 +1000,19 @@ 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 and item.bill_model.is_paid(): if item.bill_model and item.bill_model.is_paid():
models.PoItemsUploaded.objects.update_or_create( models.PoItemsUploaded.objects.update_or_create(
dealer=dealer, po=instance, item=item, dealer=dealer,
defaults={ po=instance,
"status":instance.po_status item=item,
} defaults={"status": instance.po_status},
) )
# po_item = models.PoItemsUploaded.objects.get_or_create( # po_item = models.PoItemsUploaded.objects.get_or_create(
@ -1364,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
@ -1406,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:
@ -1430,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__)
@ -70,10 +72,11 @@ def create_coa_accounts(dealer_id,**kwargs):
""" """
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

@ -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 # dashboards for manager, dealer, inventory and accounatant
path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"), path(
"dashboards/<slug:dealer_slug>/general/",
views.general_dashboard,
name="general_dashboard",
),
# dashboard for sales # dashboard for sales
path("dashboards/<slug:dealer_slug>/sales/", views.sales_dashboard, name="sales_dashboard"), 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,
@ -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
@ -1342,16 +1342,11 @@ def get_finance_data(estimate, dealer):
"discount_amount": discount, "discount_amount": discount,
"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_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"),
} }
# totals = self.calculate_totals() # totals = self.calculate_totals()
@ -1614,15 +1609,29 @@ def _post_sale_and_cogs(invoice, dealer):
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",
@ -1632,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"])
@ -1696,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,
@ -1938,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
@ -1956,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}",
@ -1974,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, "")
@ -1999,6 +2023,8 @@ def handle_payment(request, dealer):
) )
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"
@ -2518,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:
@ -2529,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(
@ -2810,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)
@ -2826,8 +2854,6 @@ 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}"

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

@ -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 %}

View File

@ -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}}

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>

View File

@ -155,12 +155,14 @@
</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('{% trans "Void Purchase Order" %}', '{% 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 }}