Compare commits

..

No commits in common. "7dbf4c4bd619240e9d6c88039f6c9ecc9360e51e" and "50589389bbec9d7e3a3ec5c6b3a378e4578d48fa" have entirely different histories.

157 changed files with 8208 additions and 8796 deletions

View File

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

View File

@ -36,3 +36,4 @@ urlpatterns += i18n_patterns(
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root = settings.STATIC_ROOT)

View File

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

View File

@ -2,7 +2,6 @@ from django.core.cache import cache
from datetime import datetime
from luhnchecker.luhn import Luhn
from django.contrib.auth.models import Permission
# from appointment.models import Service
from django.core.validators import MinLengthValidator
from django import forms
@ -58,7 +57,7 @@ from .models import (
Tasks,
Recall,
Ticket,
UserRegistration,
UserRegistration
)
from django_ledger import models as ledger_models
from django.forms import (
@ -365,14 +364,7 @@ class CarForm(
"receiving_date",
"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 = {
"id_car_make": forms.Select(attrs={"class": "form-select form-select-sm"}),
"receiving_date": forms.DateTimeInput(attrs={"type": "datetime-local"}),
@ -2131,7 +2123,8 @@ class AdditionalFinancesForm(forms.Form):
for field in self.fields.values():
if isinstance(field, forms.ModelMultipleChoiceField):
field.widget.choices = [
(obj.pk, f"{obj.name} - {obj.price:.2f}") for obj in field.queryset
(obj.pk, f"{obj.name} - {obj.price:.2f}")
for obj in field.queryset
]
@ -2148,7 +2141,6 @@ class VatRateForm(forms.ModelForm):
model = VatRate
fields = ["rate"]
class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(
label="New Password",
@ -2266,21 +2258,13 @@ class TicketResolutionForm(forms.ModelForm):
self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]
class CarDealershipRegistrationForm(forms.ModelForm):
# Add additional fields for the registration form
class Meta:
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):
customer = forms.ModelChoiceField(

View File

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

View File

@ -8,23 +8,19 @@ from django.core.management.base import BaseCommand
User = get_user_model()
class Command(BaseCommand):
help = "Deactivates expired user plans"
def handle(self, *args, **options):
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()
for user in users_without_plan:
user.is_active = False
user.save()
subject = "Your account has been deactivated"
subject = 'Your account has been deactivated'
message = """
Hello {},\n
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):
def handle(self, *args, **kwargs):
# Permission.objects.get_or_create(
# name="Can view crm",
# codename="can_view_crm",
# content_type=ContentType.objects.get_for_model(Lead),
# )
# Permission.objects.get_or_create(
# name="Can reassign lead",
# codename="can_reassign_lead",
# content_type=ContentType.objects.get_for_model(Lead),
# )
Permission.objects.get_or_create(
name="Can view crm",
codename="can_view_crm",
content_type=ContentType.objects.get_for_model(Lead),
)
Permission.objects.get_or_create(
name="Can reassign lead",
codename="can_reassign_lead",
content_type=ContentType.objects.get_for_model(Lead),
)
Permission.objects.get_or_create(
name="Can view sales",
codename="can_view_sales",
@ -47,3 +47,4 @@ class Command(BaseCommand):
codename="can_approve_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.
self.stdout.write(self.style.SUCCESS("Data reset complete."))
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
users_quota, created_u = Quota.objects.get_or_create(
codename="Users", defaults={"name": "Users", "unit": "number"}
codename="Users",
defaults={"name": "Users", "unit": "number"}
)
if created_u:
self.stdout.write(self.style.SUCCESS('Created quota: "Users"'))
cars_quota, created_c = Quota.objects.get_or_create(
codename="Cars", defaults={"name": "Cars", "unit": "number"}
codename="Cars",
defaults={"name": "Cars", "unit": "number"}
)
if created_c:
self.stdout.write(self.style.SUCCESS('Created quota: "Cars"'))
@ -43,81 +43,90 @@ class Command(BaseCommand):
# Create or get plans
basic_plan, created_bp = Plan.objects.get_or_create(
name="Basic",
defaults={"description": "basic plan", "available": True, "visible": True},
defaults={"description": "basic plan", "available": True, "visible": True}
)
if created_bp:
self.stdout.write(self.style.SUCCESS('Created plan: "Basic"'))
pro_plan, created_pp = Plan.objects.get_or_create(
name="Pro",
defaults={"description": "Pro plan", "available": True, "visible": True},
defaults={"description": "Pro plan", "available": True, "visible": True}
)
if created_pp:
self.stdout.write(self.style.SUCCESS('Created plan: "Pro"'))
enterprise_plan, created_ep = Plan.objects.get_or_create(
name="Enterprise",
defaults={
"description": "Enterprise plan",
"available": True,
"visible": True,
},
defaults={"description": "Enterprise plan", "available": True, "visible": True}
)
if created_ep:
self.stdout.write(self.style.SUCCESS('Created plan: "Enterprise"'))
# Assign quotas to plans using get_or_create to prevent duplicates
PlanQuota.objects.get_or_create(
plan=basic_plan, quota=users_quota, defaults={"value": 10000000}
plan=basic_plan,
quota=users_quota,
defaults={"value": 10000000}
)
PlanQuota.objects.get_or_create(
plan=basic_plan, quota=cars_quota, defaults={"value": 10000000}
plan=basic_plan,
quota=cars_quota,
defaults={"value": 10000000}
)
# Pro plan quotas
PlanQuota.objects.get_or_create(
plan=pro_plan, quota=users_quota, defaults={"value": 10000000}
plan=pro_plan,
quota=users_quota,
defaults={"value": 10000000}
)
PlanQuota.objects.get_or_create(
plan=pro_plan, quota=cars_quota, defaults={"value": 10000000}
plan=pro_plan,
quota=cars_quota,
defaults={"value": 10000000}
)
# Enterprise plan quotas
PlanQuota.objects.get_or_create(
plan=enterprise_plan, quota=users_quota, defaults={"value": 10000000}
plan=enterprise_plan,
quota=users_quota,
defaults={"value": 10000000}
)
PlanQuota.objects.get_or_create(
plan=enterprise_plan, quota=cars_quota, defaults={"value": 10000000}
plan=enterprise_plan,
quota=cars_quota,
defaults={"value": 10000000}
)
# Create or get pricing
basic_pricing, created_bp_p = Pricing.objects.get_or_create(
name="3 Months", defaults={"period": 90}
name="3 Months",
defaults={"period": 90}
)
pro_pricing, created_pp_p = Pricing.objects.get_or_create(
name="6 Months", defaults={"period": 180}
name="6 Months",
defaults={"period": 180}
)
enterprise_pricing, created_ep_p = Pricing.objects.get_or_create(
name="1 Year", defaults={"period": 365}
name="1 Year",
defaults={"period": 365}
)
# Assign pricing to plans
PlanPricing.objects.get_or_create(
plan=basic_plan,
pricing=basic_pricing,
defaults={"price": Decimal("2997.00")},
defaults={"price": Decimal("2997.00")}
)
PlanPricing.objects.get_or_create(
plan=pro_plan, pricing=pro_pricing, defaults={"price": Decimal("5395.00")}
plan=pro_plan,
pricing=pro_pricing,
defaults={"price": Decimal("5395.00")}
)
PlanPricing.objects.get_or_create(
plan=enterprise_plan,
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,13 +3,12 @@ from django.core.management.base import BaseCommand
from django.contrib.sites.models import Site
from django.conf import settings
class Command(BaseCommand):
help = "Update the default site domain"
help = 'Update the default site domain'
def handle(self, *args, **options):
site = Site.objects.get_current()
site.domain = settings.SITE_DOMAIN
site.name = settings.SITE_NAME
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,7 +57,6 @@ from encrypted_model_fields.fields import (
EncryptedEmailField,
EncryptedTextField,
)
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
from plans.models import Invoice
@ -67,7 +66,6 @@ from django_extensions.db.fields import RandomCharField, AutoSlugField
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Base(models.Model):
id = models.UUIDField(
unique=True,
@ -208,8 +206,11 @@ class VatRate(models.Model):
max_digits=5,
decimal_places=2,
default=Decimal("0.15"),
validators=[MinValueValidator(0.0), MaxValueValidator(1.0)],
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"),
validators=[
MinValueValidator(0.0),
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)
created_at = models.DateTimeField(auto_now_add=True)
@ -903,23 +904,16 @@ class Car(Base):
def get_active_estimates(self):
try:
qs = self.item_model.itemtransactionmodel_set.exclude(
ce_model__status="canceled"
)
qs = self.item_model.itemtransactionmodel_set.exclude(ce_model__status="canceled")
data = []
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:
data.append(x)
return data
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 []
@property
@ -1395,11 +1389,7 @@ class Dealer(models.Model, LocalizedNameMixin):
options={"quality": 80},
)
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"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -1431,7 +1421,6 @@ class Dealer(models.Model, LocalizedNameMixin):
except Exception as e:
print(e)
return None
@property
def is_plan_expired(self):
try:
@ -1466,7 +1455,6 @@ class Dealer(models.Model, LocalizedNameMixin):
def get_vendors(self):
return VendorModel.objects.filter(entity_model=self.entity)
def get_staff(self):
return Staff.objects.filter(dealer=self)
@ -1517,9 +1505,7 @@ class Staff(models.Model):
first_name = models.CharField(max_length=255, verbose_name=_("First 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(
max_length=255,
verbose_name=_("Phone Number"),
@ -2134,10 +2120,6 @@ class Lead(models.Model):
slug = RandomCharField(length=8, unique=True)
class Meta:
permissions = [
("can_view_crm", _("Can view CRM")),
("can_reassign_lead", _("Can reassign lead")),
]
verbose_name = _("Lead")
verbose_name_plural = _("Leads")
indexes = [
@ -2323,14 +2305,10 @@ class Schedule(models.Model):
help_text=_("What is the status of this schedule?"),
)
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(
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):
@ -2517,12 +2495,13 @@ class Opportunity(models.Model):
def __str__(self):
try:
if self.customer:
return f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
return (
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
)
return f"Opportunity for {self.organization.name}"
except Exception:
return f"Opportunity for car :{self.car}"
class Notes(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@ -2786,6 +2765,7 @@ class Vendor(models.Model, LocalizedNameMixin):
),
]
def __str__(self):
return self.name
@ -3175,7 +3155,7 @@ class CustomGroup(models.Model):
"notes",
"tasks",
"activity",
"additionalservices",
"additionalservices"
],
)
self.set_permissions(
@ -3292,7 +3272,7 @@ class CustomGroup(models.Model):
"payment",
"vendor",
"additionalservices",
"customer",
'customer'
],
other_perms=[
"view_car",
@ -3360,9 +3340,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_prepaid_account = models.ForeignKey(
@ -3371,9 +3349,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_unearned_account = models.ForeignKey(
@ -3382,9 +3358,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_tax_payable_account = models.ForeignKey(
@ -3393,9 +3367,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_vehicle_sale_account = models.ForeignKey(
@ -3404,9 +3376,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_additional_services_account = models.ForeignKey(
@ -3415,9 +3385,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
invoice_cost_of_good_sold_account = models.ForeignKey(
@ -3426,9 +3394,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
@ -3438,9 +3404,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
@ -3459,9 +3423,7 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
bill_unearned_account = models.ForeignKey(
@ -3470,14 +3432,10 @@ class DealerSettings(models.Model):
on_delete=models.SET_NULL,
null=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"),
)
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):
return f"Settings for {self.dealer}"
@ -3832,10 +3790,7 @@ class Ticket(models.Model):
]
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(
max_length=200, verbose_name=_("Subject"), help_text=_("Short description")
@ -3927,6 +3882,7 @@ class CarImage(models.Model):
)
class UserRegistration(models.Model):
name = models.CharField(_("Name"), max_length=255)
arabic_name = models.CharField(_("Arabic Name"), max_length=255)
@ -3936,9 +3892,7 @@ class UserRegistration(models.Model):
verbose_name=_("Phone Number"),
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)
address = models.TextField(_("Address"))
password = models.CharField(_("Password"), max_length=255,null=True,blank=True)
@ -3946,14 +3900,7 @@ class UserRegistration(models.Model):
created_at = models.DateTimeField(auto_now_add=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):
return self.email
@ -3977,7 +3924,7 @@ class UserRegistration(models.Model):
phone=self.phone_number,
crn=self.crn,
vrn=self.vrn,
address=self.address,
address=self.address
)
if dealer:

View File

@ -411,22 +411,6 @@ class BasePurchaseOrderActionActionView(
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
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
@ -1145,7 +1129,6 @@ class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, Creat
},
)
class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView):
context_object_name = "coa_model"
slug_url_kwarg = "coa_slug"

View File

@ -5,7 +5,6 @@ from django.urls import reverse
from django.contrib.auth.models import Group
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
# from appointment.models import Service
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
@ -22,7 +21,7 @@ from django_ledger.models import (
EstimateModel,
BillModel,
ChartOfAccountModel,
CustomerModel,
CustomerModel
)
from . import models
from django.utils.timezone import now
@ -137,6 +136,7 @@ def create_car_location(sender, instance, created, **kwargs):
print(f"Failed to create CarLocation for car {instance.vin}: {e}")
@receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs):
if not created:
@ -155,22 +155,20 @@ def create_ledger_entity(sender, instance, created, **kwargs):
raise Exception("Entity creation failed")
instance.entity = entity
instance.save(update_fields=["entity"])
instance.save(update_fields=['entity'])
# Create default COA
entity.create_chart_of_accounts(
assign_as_default=True, commit=True, coa_name=f"{entity.name}-COA"
assign_as_default=True,
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:
logger.error(f"💥 Failed setup for dealer {instance.id}: {e}")
# Optional: schedule retry or alert
# Create Entity
# @receiver(post_save, sender=models.Dealer)
# def create_ledger_entity(sender, instance, created, **kwargs):
@ -1000,19 +998,17 @@ def save_po(sender, instance, created, **kwargs):
instance.itemtransactionmodel_set.first().po_model.save()
except Exception as e:
pass
@receiver(post_save, sender=PurchaseOrderModel)
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]:
dealer = models.Dealer.objects.get(entity=instance.entity)
if item.bill_model and item.bill_model.is_paid():
models.PoItemsUploaded.objects.update_or_create(
dealer=dealer,
po=instance,
item=item,
defaults={"status": instance.po_status},
dealer=dealer, po=instance, item=item,
defaults={
"status":instance.po_status
}
)
# po_item = models.PoItemsUploaded.objects.get_or_create(
@ -1368,9 +1364,7 @@ def handle_car_image(sender, instance, created, **kwargs):
# )
# 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 = (
# models.CarImage.objects.filter(
# image_hash=car.get_hash, image__isnull=False
@ -1412,7 +1406,7 @@ def handle_user_registration(sender, instance, created, **kwargs):
"""
Thank you for registering with us. We will contact you shortly to complete your application.
شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك.
""",
"""
)
if instance.is_created:
@ -1436,8 +1430,7 @@ def handle_user_registration(sender, instance, created, **kwargs):
يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا.
شكرا لاختيارك لنا.
""",
)
""")
@receiver(post_save, sender=ChartOfAccountModel)

View File

@ -12,14 +12,12 @@ from django.db import transaction
from django_ledger.io import roles
from django_q.tasks import async_task
from django.core.mail import send_mail
# from appointment.models import StaffMember
from django.utils.translation import activate
from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives
# from .utils import get_accounts_data, create_account
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
@ -32,7 +30,7 @@ from inventory.models import (
CarReservation,
CarStatusChoices,
CarImage,
Car,
Car
)
logger = logging.getLogger(__name__)
@ -72,11 +70,10 @@ def create_coa_accounts(dealer_id, **kwargs):
"""
from .models import Dealer
from .utils import get_accounts_data, create_account
try:
dealer = Dealer.objects.get(pk=dealer_id)
entity = dealer.entity
coa_slug = kwargs.get("coa_slug", None)
coa_slug = kwargs.get('coa_slug', None)
if not entity:
logger.error(f"❌ No entity for dealer {dealer_id}")
return False
@ -85,9 +82,7 @@ def create_coa_accounts(dealer_id, **kwargs):
try:
coa = entity.get_coa_model_qs().get(slug=coa_slug)
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
else:
coa = entity.get_default_coa()
@ -97,13 +92,10 @@ def create_coa_accounts(dealer_id, **kwargs):
return False
# 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 = [
acc for acc in get_accounts_data() if acc["code"] not in existing_codes
acc for acc in get_accounts_data()
if acc["code"] not in existing_codes
]
if not accounts_to_create:
@ -130,7 +122,6 @@ def create_coa_accounts(dealer_id, **kwargs):
logger.error(f"💥 Task failed for dealer {dealer_id}: {e}")
raise # Let Django-Q handle retry if configured
def retry_entity_creation(dealer_id, retry_count=0):
"""
Retry entity creation if initial attempt failed
@ -173,10 +164,8 @@ def retry_entity_creation(dealer_id, retry_count=0):
async_task(
"inventory.tasks.retry_entity_creation",
dealer_id=dealer_id,
retry_count=retry_count + 1,
retry_count=retry_count + 1
)
# def create_coa_accounts(**kwargs):
# logger.info("creating all accounts are created")
# instance = kwargs.get("dealer")

View File

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

View File

@ -10,17 +10,14 @@ urlpatterns = [
# main URLs
path("", views.WelcomeView, name="welcome"),
# path("signup/", views.dealer_signup, 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('signup/', views.CarDealershipSignUpView.as_view(), name='account_signup'),
path('success/', TemplateView.as_view(template_name='account/success.html'), name='registration_success'),
path("", views.HomeView, name="home"),
# path('refund-policy/',views.refund_policy,name='refund_policy'),
path("<slug:dealer_slug>/", views.HomeView, name="home"),
# Tasks
path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
@ -47,18 +44,13 @@ urlpatterns = [
views.assign_car_makes,
name="assign_car_makes",
),
#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
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(
"<slug:dealer_slug>/cars/aging-inventory/list",
views.aging_inventory_list_view,
@ -794,11 +786,7 @@ urlpatterns = [
views.EstimateDetailView.as_view(),
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(
"<slug:dealer_slug>/sales/estimates/create/",
views.create_estimate,
@ -955,6 +943,7 @@ urlpatterns = [
views.ItemServiceUpdateView.as_view(),
name="item_service_update",
),
path(
"<slug:dealer_slug>/items/services/<int:pk>/detail/",
views.ItemServiceDetailView.as_view(),
@ -1123,47 +1112,32 @@ urlpatterns = [
name="entity-ic-date",
),
# 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(),
name="coa-list",
),
path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/",
name='coa-list'),
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/',
views.ChartOfAccountModelListView.as_view(inactive=True),
name="coa-list-inactive",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/create/",
name='coa-list-inactive'),
path('<slug:dealer_slug>/<slug:entity_slug>/create/',
views.ChartOfAccountModelCreateView.as_view(),
name="coa-create",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/",
name='coa-create'),
path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/',
views.ChartOfAccountModelListView.as_view(),
name="coa-detail",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/",
name='coa-detail'),
path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/',
views.ChartOfAccountModelUpdateView.as_view(),
name="coa-update",
),
name='coa-update'),
# ACTIONS....
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"),
name="coa-action-mark-as-default",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"),
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",
),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
name='coa-action-mark-as-default'),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
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...
# Entities...
path(
@ -1339,80 +1313,42 @@ urlpatterns = [
views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void",
),
# reports
path(
"<slug:dealer_slug>/purchase-report/",
views.purchase_report_view,
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(
"<slug:dealer_slug>/car-sale-report/",
views.car_sale_report_view,
name="car-sale-report",
),
path(
"<slug:dealer_slug>/car-sale-report/get_filtered_choices/",
views.get_filtered_choices,
name="get_filtered_choices",
),
path(
"car-sale-report/<slug:dealer_slug>/csv/",
views.car_sale_report_csv_export,
name="car-sale-report-csv-export",
),
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",
),
path('<slug:dealer_slug>/car-sale-report/get_filtered_choices/',views.get_filtered_choices,name='get_filtered_choices'),
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'),
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
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
path("help_center/view/", views.help_center, name="help_center"),
path(
"<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list"
),
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/view/', views.help_center, name='help_center'),
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'),
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("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"

View File

@ -27,7 +27,7 @@ from django_ledger.models import (
VendorModel,
AccountModel,
EntityModel,
ChartOfAccountModel,
ChartOfAccountModel
)
from django.core.files.base import ContentFile
from django_ledger.models.items import ItemModel
@ -1342,11 +1342,16 @@ def get_finance_data(estimate, dealer):
"discount_amount": discount,
"additional_services": additional_services,
"final_price": discounted_price + vat_amount,
"total_services_vat": total_services_vat,
"total_services_amount":total_services_amount,
"total_services_amount_":total_services_amount_,
"total_vat": total_vat,
"grand_total": discounted_price + total_vat + additional_services.get("total"),
}
# totals = self.calculate_totals()
@ -1609,29 +1614,15 @@ def _post_sale_and_cogs(invoice, dealer):
coa:ChartOfAccountModel = entity.get_default_coa()
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
if not add_rev:
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:
add_rev = coa.create_account(
code="4020",
@ -1641,28 +1632,16 @@ def _post_sale_and_cogs(invoice, dealer):
active=True,
)
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.save()
except Exception as e:
logger.error(f"error find or create additional services account {e}")
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"
)
cogs_acc = (
dealer.settings.invoice_cost_of_good_sold_account
or entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.COGS)
.first()
)
raise Exception("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_additionals_price = Decimal(data["additional_services"]["total"])
@ -1717,12 +1696,11 @@ def _post_sale_and_cogs(invoice, dealer):
# tx_type='credit'
# )
if car.get_additional_services_amount > 0:
# Cr Sales Additional Services
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:
TransactionModel.objects.create(
journal_entry=je_sale,
@ -1960,7 +1938,7 @@ def handle_payment(request, dealer):
}
# 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:
raise ValueError("No pending plan found in session")
from plans.models import PlanPricing
@ -1978,8 +1956,7 @@ def handle_payment(request, dealer):
"dealer_slug": dealer.slug,
}
payload = json.dumps(
{
payload = json.dumps({
"amount": total,
"currency": "SAR",
"description": f"Payment for plan {pp.plan.name}",
@ -1997,8 +1974,7 @@ def handle_payment(request, dealer):
"save_card": False,
},
"metadata": metadata,
}
)
})
headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "")
@ -2023,8 +1999,6 @@ def handle_payment(request, dealer):
)
logger.info(f"Payment initiated: {data}")
return data["source"]["transaction_url"],None
# def handle_payment(request, order):
# logger.info(f"Handling payment for order {order}")
# url = "https://api.moyasar.com/v1/payments"
@ -2544,9 +2518,10 @@ def create_account(entity, coa, account_data):
logger.info(f"Created account: {account}")
if account:
account.role_default = account_data.get("default", False)
account.save(update_fields=["role_default"])
account.save(update_fields=['role_default'])
return True
except IntegrityError:
return True # Already created by race condition
except Exception as e:
@ -2554,7 +2529,6 @@ def create_account(entity, coa, account_data):
return False
# def create_account(entity, coa, account_data):
# try:
# account = entity.create_account(
@ -2836,9 +2810,7 @@ def generate_car_image_simple(car):
# Save the resized image
logger.info(f" {car.vin}")
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",
) as f:
f.write(resized_data)
@ -2854,6 +2826,8 @@ def generate_car_image_simple(car):
return {"success": False, "error": error_msg}
def create_estimate_(dealer,car,customer):
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}"

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -335,7 +335,7 @@
</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="col-12 col-lg-auto">
<div id="timeline" class="timeline-basic mb-9">
<div class="timeline-basic mb-9">
{% for activity in activities %}
<div class="timeline-item">
<div class="row g-3">
@ -354,8 +354,6 @@
<span class="fa-solid fa-users text-danger fs-8"></span>
{% elif activity.activity_type == "whatsapp" %}
<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 %}
</div>
{% if forloop.last %}

View File

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

View File

@ -542,7 +542,7 @@
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div id="estimateModalBody" style="padding: 20px;">
<div id="estimateModalBody" class="main-modal-body" style="padding: 20px;">
<form action="{% url 'create_estimate_for_car' request.dealer.slug car.slug %}" method="post">
{% csrf_token %}
{{estimate_form|crispy}}

View File

@ -49,8 +49,6 @@
<span class="badge badge-phoenix badge-phoenix-success">
{% elif bill.is_canceled %}
<span class="badge badge-phoenix badge-phoenix-danger">
{% elif bill.is_void %}
<span class="badge badge-phoenix badge-phoenix-secondary">
{% endif %}
{{ bill.bill_status }}
</span>

View File

@ -155,14 +155,12 @@
</div>
{% endif %}
{% endif %}
{% 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 %}
{% if po_model.can_void %}
<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')">
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
</button>
{% endif %} {% endcomment %}
{% endif %}
{% if po_model.can_cancel %}
<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')">

View File

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

View File

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