forms validations for vat rate

This commit is contained in:
Faheed 2025-09-10 12:22:54 +03:00
commit acf335e2ec
33 changed files with 2316 additions and 1416 deletions

View File

@ -45,20 +45,4 @@ application = ProtocolTypeRouter(
)
),
}
)
try:
from django.conf import settings
from django.contrib.sites.models import Site
if not settings.DEBUG:
site = Site.objects.get(id=settings.SITE_ID)
if site.domain != settings.PRODUCTION_DOMAIN:
site.domain = settings.PRODUCTION_DOMAIN
site.name = settings.SITE_NAME
site.save()
except Exception as e:
# Log error but don't crash the app
if settings.DEBUG:
print(f"Site configuration error in WSGI: {e}")
)

View File

@ -1,7 +1,7 @@
#!/bin/bash
sudo apt-get update && sudo apt-get install libgl1 libglib2.0-dev libzbar0 cmake build-essential xmlsec1 libxmlsec1-dev pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl libssl-dev -y
sudo apt-get update && sudo apt-get install gettext libgl1 libglib2.0-dev libzbar0 cmake build-essential xmlsec1 libxmlsec1-dev pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl libssl-dev -y
pip install --upgrade pip
pip install -r requirements_dev.txt
./apply_initial_migrations.sh

View File

@ -2118,6 +2118,15 @@ class AdditionalFinancesForm(forms.Form):
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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
]
class VatRateForm(forms.ModelForm):
rate = forms.DecimalField(

View File

@ -17,6 +17,7 @@ def check_create_coa_accounts(task):
try:
dealer_id = task.kwargs.get('dealer_id')
coa_slug = task.kwargs.get('coa_slug', None)
if not dealer_id:
logger.error("No dealer_id in task kwargs")
return
@ -29,7 +30,13 @@ def check_create_coa_accounts(task):
logger.error(f"No entity for dealer {dealer_id}")
return
coa = entity.get_default_coa()
if coa_slug:
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.id}: {e}")
else:
coa = entity.get_default_coa()
if not coa:
logger.error(f"No COA for entity {entity.id}")
return

View File

@ -0,0 +1,39 @@
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from inventory.tasks import send_email
from django.contrib.auth import get_user_model
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)
)
count = users_without_plan.count()
for user in users_without_plan:
user.is_active = False
user.save()
subject = 'Your account has been deactivated'
message = """
Hello {},\n
Your account has been deactivated, please contact us at {} if you have any questions.
Regards,\n
Tenhal Team
""".format(user.dealer.name, settings.DEFAULT_FROM_EMAIL)
from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = user.email
send_email(from_email, recipient_list,subject, message)
self.stdout.write(
self.style.SUCCESS(
f"Successfully deactivated {count} dealers who created account but dont have userplan"
)
)

View File

@ -57,7 +57,7 @@ class Command(BaseCommand):
},
)
send_email(
"noreply@yourdomain.com",
settings.DEFAULT_FROM_EMAIL,
inv.customer.email,
subject,
message,
@ -118,7 +118,7 @@ class Command(BaseCommand):
# send email to customer
send_email(
"noreply@yourdomain.com",
settings.DEFAULT_FROM_EMAIL,
inv.customer.email,
subject,
message,

View File

@ -25,7 +25,7 @@ class Command(BaseCommand):
self.deactivate_expired_plans()
# 3. Clean up old incomplete orders
self.cleanup_old_orders()
# self.cleanup_old_orders()
self.stdout.write("Maintenance completed!")
@ -58,9 +58,19 @@ class Command(BaseCommand):
def deactivate_expired_plans(self):
"""Deactivate plans that have expired (synchronous)"""
expired_plans = UserPlan.objects.filter(
active=True, expire__lt=timezone.now().date()
active=True, expire__lte=timezone.now() - timedelta(days=7)
)
for plan in expired_plans:
# try:
if dealer := getattr(plan.user,"dealer", None):
dealer.user.is_active = False
dealer.user.save()
for staff in dealer.get_staff():
staff.deactivate_account()
count = expired_plans.update(active=False)
# except:
# logger.warning(f"User {plan.user_id} does not exist")
self.stdout.write(f"Deactivated {count} expired plans")
def cleanup_old_orders(self):

View File

@ -0,0 +1,14 @@
# management/commands/update_site.py
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'
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}'))

View File

@ -169,7 +169,7 @@ class DealerSlugMiddleware:
"/ar/help_center/",
"/en/help_center/",
]
if request.path in paths:
return None

View File

@ -8,7 +8,7 @@ from decimal import Decimal
from django.urls import reverse
from django.utils.text import slugify
from django.utils import timezone
from django.core.validators import MinValueValidator
from django.core.validators import MinValueValidator,MaxValueValidator
import hashlib
from django.db import models
from datetime import timedelta
@ -206,12 +206,25 @@ class UnitOfMeasure(models.TextChoices):
class VatRate(models.Model):
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE)
rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.15"))
rate = models.DecimalField(
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%)")
)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Rate: {self.rate}%"
return f"Rate: {self.rate * 100}%"
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class CarType(models.IntegerChoices):
@ -1365,7 +1378,7 @@ class Dealer(models.Model, LocalizedNameMixin):
options={"quality": 80},
)
entity = models.ForeignKey(
EntityModel, on_delete=models.SET_NULL, null=True, blank=True
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"))
@ -1397,6 +1410,13 @@ class Dealer(models.Model, LocalizedNameMixin):
except Exception as e:
print(e)
return None
@property
def is_plan_expired(self):
try:
return UserPlan.objects.get(user=self.user, active=True).is_expired()
except Exception as e:
logger.error(e)
return True
@property
def customers(self):
@ -1424,6 +1444,8 @@ 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)
@property
def is_staff_exceed_quota_limit(self):

View File

@ -20,6 +20,7 @@ from django_ledger.models import (
PurchaseOrderModel,
EstimateModel,
BillModel,
ChartOfAccountModel,
)
from . import models
from django.utils.timezone import now
@ -71,13 +72,12 @@ User = get_user_model()
# instance.save()
# check with marwan
# @receiver(post_save, sender=models.Car)
# def create_dealers_make(sender, instance, created, **kwargs):
# if created:
# models.DealersMake.objects.get_or_create(
# dealer=instance.dealer, car_make=instance.id_car_make
# )
@receiver(post_save, sender=models.Car)
def create_dealers_make(sender, instance, created, **kwargs):
if created:
models.DealersMake.objects.get_or_create(
dealer=instance.dealer, car_make=instance.id_car_make
)
@receiver(post_save, sender=models.Car)
@ -1425,3 +1425,20 @@ def handle_user_registration(sender, instance, created, **kwargs):
شكرا لاختيارك لنا.
""")
@receiver(post_save, sender=ChartOfAccountModel)
def handle_chart_of_account(sender, instance, created, **kwargs):
if created:
try:
dealer = instance.entity.dealers.first()
async_task(
func="inventory.tasks.create_coa_accounts",
dealer_id=dealer.pk, # Pass ID instead of object
coa_slug=instance.slug,
hook="inventory.hooks.check_create_coa_accounts",
ack_failure=True, # Ensure task failures are acknowledged
sync=False # Explicitly set to async
)
except Exception as e:
logger.error(f"Error handling chart of account: {e}")

View File

@ -72,23 +72,32 @@ def create_coa_accounts(dealer_id, **kwargs):
max_retries = 3
retry_delay = 2 # seconds
coa_slug = kwargs.get('coa_slug', None)
logger.info(f"chart of account model slug {coa_slug}")
logger.info(f"Attempting to create accounts for dealer {dealer_id}")
for attempt in range(max_retries):
try:
logger.info(f"Attempt {attempt + 1} to create accounts for dealer {dealer_id}")
# Get fresh instance from database
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
instance = Dealer.objects.get(pk=dealer_id)
entity = instance.entity
if not entity:
logger.error(f"No entity found for dealer {dealer_id}")
return False
coa = entity.get_default_coa()
if coa_slug:
try:
coa = entity.get_coa_model_qs().get(slug=coa_slug)
logger.info(f"COA with slug {coa_slug} found for entity {entity.pk}")
except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
else:
coa = entity.get_default_coa()
logger.info(f"Default COA found for entity {entity.pk}")
if not coa:
logger.error(f"No COA found for entity {entity.id}")
logger.error(f"No COA found for entity {entity.pk}")
return False
logger.info("Creating default accounts")
@ -112,11 +121,11 @@ def create_coa_accounts(dealer_id, **kwargs):
else:
logger.error(f"All {max_retries} attempts failed for dealer {dealer_id}")
# Schedule a cleanup or notification task
async_task(
"inventory.tasks.handle_account_creation_failure",
dealer_id=dealer_id,
error=str(e)
)
# async_task(
# "inventory.tasks.handle_account_creation_failure",
# dealer_id=dealer_id,
# error=str(e)
# )
return False
def retry_entity_creation(dealer_id, retry_count=0):

View File

@ -25,6 +25,7 @@ from django_ledger.models import (
InvoiceModel,
BillModel,
VendorModel,
AccountModel
)
from django.core.files.base import ContentFile
from django_ledger.models.items import ItemModel
@ -2391,15 +2392,17 @@ def create_account(entity, coa, account_data):
Create account with proper validation and error handling
"""
try:
# Check if account already exists
# existing_account = AccountModel.objects.filter(coa_model=coa,code=account_data["code"])
existing_account = entity.get_all_accounts().filter(
coa_model=coa,
code=account_data["code"]
).first()
)
if existing_account:
logger.info(f"Account already exists: {account_data['code']}")
return True
logger.info(f"Creating account: {account_data['code']}")
account = entity.create_account(
coa_model=coa,
code=account_data["code"],
@ -2408,6 +2411,7 @@ def create_account(entity, coa, account_data):
balance_type=_(account_data["balance_type"]),
active=True,
)
logger.info(f"Successfully created account: {account_data['code']}")
if account:
account.role_default = account_data["default"]

View File

@ -3640,17 +3640,17 @@ class UserCreateView(
def form_valid(self, form):
staff = form.save(commit=False)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
if dealer.is_staff_exceed_quota_limit:
messages.error(
self.request,
_(
"You have reached the maximum number of staff users allowed for your plan"
),
)
return self.form_invalid(form)
# if dealer.is_staff_exceed_quota_limit:
# messages.error(
# self.request,
# _(
# "You have reached the maximum number of staff users allowed for your plan"
# ),
# )
# return self.form_invalid(form)
email = form.cleaned_data["email"]
if models.Staff.objects.filter(user__email=email).exists():
if models.Staff.objects.filter(dealer=dealer,user__email=email).exists() or models.Dealer.objects.filter(user__email=email).exists():
messages.error(
self.request,
_(
@ -4201,7 +4201,7 @@ class BankAccountCreateView(
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form = super().get_form(form_class)
account_qs = dealer.entity.get_all_accounts().filter(
account_qs = dealer.entity.get_default_coa_accounts().filter(
role__in=[
roles.ASSET_CA_CASH,
roles.LIABILITY_CL_ACC_PAYABLE,
@ -4296,7 +4296,7 @@ class BankAccountUpdateView(
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form = super().get_form(form_class)
account_qs = dealer.entity.get_all_accounts().filter(
account_qs = dealer.entity.get_default_coa_accounts().filter(
role__in=[
roles.ASSET_CA_CASH,
roles.LIABILITY_CL_ACC_PAYABLE,
@ -5926,6 +5926,9 @@ def PaymentCreateView(request, dealer_slug, pk):
if not model.is_approved():
model.mark_as_approved(user_model=entity.admin)
if amount < invoice.amount_due:
messages.error(request, _("Amount cannot be less than due amount"))
return response
if model.amount_paid == model.amount_due:
messages.error(request, _("fully paid"))
return response
@ -6789,8 +6792,10 @@ def delete_note(request, dealer_slug, pk):
"""
try:
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
print(note)
if isinstance(note.content_object, models.Lead):
if isinstance(note.content_object, models.Customer):
url = "customer_detail"
slug = note.content_object.slug
elif isinstance(note.content_object, models.Lead):
url = "lead_detail"
slug = note.content_object.slug
if hasattr(note.content_object, "opportunity"):
@ -9810,7 +9815,7 @@ def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk):
def pricing_page(request, dealer_slug):
dealer=get_object_or_404(models.Dealer, slug=dealer_slug)
vat = models.VatRate.objects.filter(dealer=dealer).first()
if not dealer.active_plan:
if not hasattr(dealer.user,'userplan') or dealer.is_plan_expired:
plan_list = PlanPricing.objects.annotate(
price_with_tax=Round(F('price') * vat.rate + F('price'), 2)
).all()
@ -9887,7 +9892,7 @@ def payment_callback(request, dealer_slug):
UserPlan.objects.create(
user=order.user,
plan=order.plan,
expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period)
# expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period)
)
logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.")
else:
@ -9923,7 +9928,16 @@ def payment_callback(request, dealer_slug):
history.status = "failed"
history.save()
return render(request, "payment_failed.html", {"message": "Plan activation error"})
finally:
if dealer := getattr(order.user,"dealer", None):
if not dealer.user.is_active:
dealer.user.is_active = True
dealer.user.save()
for staff in dealer.get_staff():
if not staff.user.is_active:
staff.activate_account()
logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
elif payment_status == "failed":
logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
history.status = "failed"
@ -10618,7 +10632,9 @@ def InventoryItemCreateView(request, dealer_slug):
messages.error(request, _("Inventory item already exists"))
return response
uom = entity.get_uom_all().get(name="Unit")
uom = entity.get_uom_all().filter(name="Unit").first()
if not uom:
uom = entity.create_uom(name="Unit", unit_abbr="unit")
entity.create_item_inventory(
name=inventory_name,
uom_model=uom,
@ -11474,15 +11490,12 @@ def staff_password_reset_view(request, dealer_slug, user_pk):
if request.method == 'POST':
form = forms.CustomSetPasswordForm(staff.user, request.POST)
if form.is_valid():
print(form.cleaned_data['new_password1'])
print(form.cleaned_data['new_password2'])
form.save()
messages.success(request, _('Your password has been set. You may go ahead and log in now.'))
return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug)
else:
messages.error(request, _('Invalid password. Please try again.'))
messages.error(request, _(f'Invalid password. {str(form.errors)}'))
form = forms.CustomSetPasswordForm(staff.user)
return render(request, 'users/user_password_reset.html', {'form': form})

View File

@ -26,4 +26,7 @@ python3 manage.py tenhal_plan
python3 manage.py set_custom_permissions
echo "Updating site domain"
python3 manage.py update_site
echo "Done"

Binary file not shown.

File diff suppressed because it is too large Load Diff

162
requirements.prod.txt Normal file
View File

@ -0,0 +1,162 @@
annotated-types==0.7.0
anyio==4.9.0
arrow==1.3.0
asgiref==3.9.1
attrs==25.3.0
autobahn==24.4.2
Automat==25.4.16
Babel==2.15.0
beautifulsoup4==4.13.4
blessed==1.21.0
cattrs==25.1.1
certifi==2025.7.9
cffi==1.17.1
channels==4.2.2
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
constantly==23.10.4
crispy-bootstrap5==2025.6
cryptography==45.0.5
cssbeautifier==1.15.4
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
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
django-import-export==4.3.8
django-js-asset==3.1.2
django-ledger==0.7.11
django-manager-utils==3.1.5
django-next-url-mixin==0.4.0
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
django-sequences==3.0
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
Faker==37.4.0
fleming==0.7.0
fonttools==4.58.5
fpdf==1.7.2
fpdf2==2.8.3
greenlet==3.2.3
gunicorn==23.0.0
h11==0.16.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.9
httpx==0.28.1
hyperframe==6.1.0
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
jsonpatch==1.33
jsonpointer==3.0.0
jwt==1.4.0
langchain==0.3.26
langchain-core==0.3.68
langchain-ollama==0.3.4
langchain-text-splitters==0.3.8
langsmith==0.4.4
luhnchecker==0.0.12
Markdown==3.8.2
markdown-it-py==3.0.0
mdurl==0.1.2
num2words==0.5.14
numpy==2.3.1
ofxtools==0.9.5
ollama==0.5.1
openai==1.93.3
opencv-python==4.11.0.86
orjson==3.10.18
packaging==24.2
pandas==2.3.1
pathspec==0.12.1
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
Pygments==2.19.2
pymongo==4.14.1
pyOpenSSL==25.1.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-slugify==8.0.4
python-stdnum==2.1
pytz==2025.2
pyvin==0.0.2
PyYAML==6.0.2
pyzbar==0.1.9
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
sniffio==1.3.1
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3
suds==1.2.0
swapper==1.3.0
tablib==3.8.0
tenacity==9.1.2
text-unidecode==1.3
tqdm==4.67.1
Twisted==25.5.0
txaio==25.6.1
types-python-dateutil==2.9.0.20250708
typing-inspection==0.4.1
typing_extensions==4.14.1
tzdata==2025.2
urllib3==2.5.0
uvicorn==0.35.0
uvicorn-worker==0.3.0
wcwidth==0.2.13
whitenoise==6.9.0
zope.interface==7.2
zstandard==0.23.0

View File

@ -1,162 +1,374 @@
aiofiles==24.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiohttp-retry==2.9.1
aiosignal==1.3.2
alabaster==1.0.0
albucore==0.0.23
albumentations==2.0.5
annotated-types==0.7.0
anyio==4.9.0
arabic-reshaper==3.0.0
arrow==1.3.0
asgiref==3.9.1
astor==0.8.1
astroid==3.3.9
attrs==25.3.0
autobahn==24.4.2
Automat==25.4.16
autopep8==2.3.2
Babel==2.15.0
beautifulsoup4==4.13.4
bidict==0.23.1
binaryornot==0.4.4
bleach==6.2.0
blessed==1.21.0
blinker==1.9.0
Brotli==1.1.0
cattrs==25.1.1
certifi==2025.7.9
cffi==1.17.1
channels==4.2.2
chardet==5.2.0
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
constantly==23.10.4
commonmark==0.9.1
contourpy==1.3.1
cookiecutter==2.6.0
crispy-bootstrap5==2025.6
cryptography==45.0.5
cssbeautifier==1.15.4
daphne==4.2.1
cssselect2==0.8.0
cycler==0.12.1
Cython==3.0.12
datastar-py==0.6.2
decorator==5.2.1
defusedxml==0.7.1
desert==2020.11.18
diff-match-patch==20241021
dill==0.3.9
distlib==0.3.9
distro==1.9.0
dj-rest-auth==7.0.1
dj-shop-cart==7.1.1
Django==5.2.4
django-admin-sortable2==1.0.4
django-allauth==65.10.0
django-angular==2.3.1
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-classy-tags==3.0.1
django-cms==3.11.3
django-cors-headers==4.7.0
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-entangled==0.6.2
django-extensions==4.1
django-extra-views==0.14.0
django-filer==3.0.3
django-filter==25.1
django-formtools==2.4
django-fsm==3.0.0
django-fsm-admin==1.2.5
django-haystack==3.3.0
django-imagekit==5.0.0
django-import-export==4.3.8
django-ipware==7.0.1
django-js-asset==3.1.2
django-ledger==0.7.6.1
django-manager-utils==3.1.5
django-model-utils==5.0.0
django-money==3.5.3
django-next-url-mixin==0.4.0
django-nine==0.2.7
django-nonefield==0.4
django-ordered-model==3.7.4
django-oscar==3.2.5
django-pdf-actions==0.1.44
django-phonenumber-field==8.0.0
django-picklefield==3.3
django-plans==2.0.0
django-prometheus==2.4.1
django-polymorphic==3.1.0
django-post-office==3.6.3
django-prometheus==2.3.1
django-q2==1.8.0
django-query-builder==3.2.0
django-rest-auth==0.9.5
django-schema-graph==3.1.0
django-sekizai==3.0.1
django-select2==7.10.0
django-sequences==3.0
django-shop==1.2.4
django-silk==5.3.2
django-sms==0.7.0
django-sslserver==0.22
django-tables2==2.7.5
django-tailwind==4.0.1
django-treebeard==4.7.1
django-view-breadcrumbs==2.5.1
django-viewflow==2.2.12
django-widget-tweaks==1.5.0
djangocms-admin-style==3.3.1
djangocms-cascade==1.3.7
djangocms-text-ckeditor==5.1.7
djangorestframework==3.16.0
djangorestframework_simplejwt==5.5.0
djangoviz==0.1.1
djhtml==3.0.8
djlint==1.36.4
dnspython==2.7.0
docopt==0.6.2
docutils==0.21.2
easy-thumbnails==2.9
ecdsa==0.19.1
EditorConfig==0.17.1
emoji==2.14.1
et_xmlfile==2.0.0
factory-boy==3.2.1
Faker==37.4.0
fastapi==0.115.12
filelock==3.18.0
fire==0.7.0
fleming==0.7.0
fonttools==4.58.5
fpdf==1.7.2
fpdf2==2.8.3
frozenlist==1.5.0
fsspec==2025.3.0
gprof2dot==2024.6.6
graphqlclient==0.2.4
greenlet==3.2.3
gunicorn==23.0.0
h11==0.16.0
h2==4.2.0
hpack==4.1.0
hstspreload==2025.1.1
html5lib==1.1
htmx==0.0.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
httpx-ws==0.7.2
hyperframe==6.1.0
hyperlink==21.0.0
icalendar==6.3.1
idna==3.10
incremental==24.7.2
iron-core==1.2.1
iron-mq==0.9
ifaddr==0.2.0
imageio==2.37.0
imagesize==1.4.1
imgaug==0.4.0
iso4217==1.12.20240625
isodate==0.7.2
isort==6.0.1
itsdangerous==2.2.0
Jinja2==3.1.6
jiter==0.10.0
joblib==1.4.2
jsbeautifier==1.15.4
json5==0.12.0
jsonfield==3.1.0
jsonpatch==1.33
jsonpointer==3.0.0
jwt==1.4.0
kiwisolver==1.4.8
langchain==0.3.26
langchain-core==0.3.68
langchain-ollama==0.3.4
langchain-text-splitters==0.3.8
langsmith==0.4.4
lazy_loader==0.4
ledger==1.0.1
libretranslatepy==2.1.4
lmdb==1.6.2
lmstudio==1.4.1
luhnchecker==0.0.12
lxml==5.3.1
Markdown==3.8.2
markdown-it-py==3.0.0
markdown2==2.5.3
MarkupSafe==3.0.2
marshmallow==3.26.1
matplotlib==3.10.1
mccabe==0.7.0
mdurl==0.1.2
MouseInfo==0.1.3
mpmath==1.3.0
msgspec==0.19.0
multidict==6.2.0
mypy-extensions==1.0.0
networkx==3.4.2
newrelic==10.7.0
nicegui==2.13.0
nltk==3.9.1
num2words==0.5.14
numpy==2.3.1
nvidia-cublas-cu12==12.4.5.8
nvidia-cuda-cupti-cu12==12.4.127
nvidia-cuda-nvrtc-cu12==12.4.127
nvidia-cuda-runtime-cu12==12.4.127
nvidia-cudnn-cu12==9.1.0.70
nvidia-cufft-cu12==11.2.1.3
nvidia-curand-cu12==10.3.5.147
nvidia-cusolver-cu12==11.6.1.9
nvidia-cusparse-cu12==12.3.1.170
nvidia-cusparselt-cu12==0.6.2
nvidia-nccl-cu12==2.21.5
nvidia-nvjitlink-cu12==12.4.127
nvidia-nvtx-cu12==12.4.127
oauthlib==3.2.2
ofxtools==0.9.5
ollama==0.5.1
openai==1.93.3
opencv-contrib-python==4.11.0.86
opencv-python==4.11.0.86
opencv-python-headless==4.11.0.86
openpyxl==3.1.5
opt_einsum==3.4.0
orjson==3.10.18
outcome==1.3.0.post0
packaging==24.2
pandas==2.3.1
pango==0.0.1
passlib==1.7.4
pathspec==0.12.1
pdfkit==1.0.0
phonenumbers==8.13.42
pilkit==3.0
pillow==10.4.0
priority==1.3.0
prometheus_client==0.22.1
pipenv==2024.4.1
platformdirs==4.3.7
prometheus_client==0.21.1
propcache==0.3.0
protobuf==6.30.1
pscript==0.7.7
psycopg-binary==3.2.6
psycopg2-binary==2.9.10
purl==1.6
py-moneyed==3.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
PyAutoGUI==0.9.54
pyclipper==1.3.0.post6
pycodestyle==2.12.1
pycountry==24.6.1
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
pydotplus==2.0.2
pydyf==0.11.0
PyGetWindow==0.0.9
Pygments==2.19.2
pymongo==4.14.1
pyOpenSSL==25.1.0
PyJWT==2.9.0
pylint==3.3.5
PyMsgBox==1.0.9
pyparsing==3.2.1
pypdf==5.4.0
PyPDF2==3.0.1
pyperclip==1.9.0
pyphen==0.17.2
pypng==0.20220715.0
PyRect==0.2.0
PyScreeze==1.0.1
pyserial==3.5
PySocks==1.7.1
python-bidi==0.6.6
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-docx==1.1.2
python-dotenv==1.0.1
python-engineio==4.11.2
python-ipware==3.0.0
python-jose==3.5.0
python-multipart==0.0.20
python-openid==2.2.5
python-slugify==8.0.4
python-socketio==5.12.1
python-stdnum==2.1
python3-saml==1.16.0
python3-xlib==0.15
pytweening==1.2.0
pytz==2025.2
pyvin==0.0.2
PyYAML==6.0.2
pyzbar==0.1.9
qrcode==8.0
RapidFuzz==3.12.2
redis==6.2.0
regex==2024.11.6
reportlab==4.3.1
requests==2.32.4
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
rsa==4.9.1
rubicon-objc==0.5.0
ruff==0.12.2
service-identity==24.2.0
sacremoses==0.1.1
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.15.2
selenium==4.29.0
sentencepiece==0.2.0
setuptools==80.9.0
shapely==2.0.7
simple-websocket==1.1.0
simsimd==6.2.1
six==1.17.0
sniffio==1.3.1
snowballstemmer==2.2.0
sorl-thumbnail==12.9.0
sortedcontainers==2.4.0
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3
stanza==1.10.1
starlette==0.46.1
stringzilla==3.12.3
suds==1.2.0
svglib==1.5.1
swapper==1.3.0
sympy==1.13.1
tablib==3.8.0
tenacity==9.1.2
termcolor==2.5.0
text-unidecode==1.3
threadpoolctl==3.6.0
tifffile==2025.3.13
tinycss2==1.4.0
tinyhtml5==2.0.0
tomli==2.2.1
tomlkit==0.13.2
torch==2.6.0
tqdm==4.67.1
Twisted==25.5.0
txaio==25.6.1
trio==0.29.0
trio-websocket==0.12.2
triton==3.2.0
types-python-dateutil==2.9.0.20250708
typing-inspect==0.9.0
typing-inspection==0.4.1
typing_extensions==4.14.1
tzdata==2025.2
Unidecode==1.3.8
upgrade-requirements==1.7.0
urllib3==2.5.0
uvicorn==0.35.0
uvicorn-worker==0.3.0
uv==0.6.14
uvicorn==0.34.0
uvloop==0.21.0
vbuild==0.8.2
virtualenv==20.30.0
vishap==0.1.5
vpic-api==0.7.4
watchfiles==1.0.4
wcwidth==0.2.13
weasyprint==64.1
webencodings==0.5.1
websocket-client==1.8.0
websockets==15.0.1
Werkzeug==3.1.3
whitenoise==6.9.0
zope.interface==7.2
wikipedia==1.4.0
wsproto==1.2.0
xmlsec==1.3.15
yarl==1.18.3
zopfli==0.2.3.post1
zstandard==0.23.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,4 +1,4 @@
{% extends "allauth/layouts/entrance.html" %}
{% extends "base.html" %}
{% load i18n %}
{% load allauth %}
{% block head_title %}

View File

@ -265,74 +265,13 @@
</div>
</div>
</div>
<div class="col-sm-auto">
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center border-start-sm ps-sm-5 border-translucent">
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0"
style="width:32px;
height:32px">
<span class="text-success-dark icon-saudi_riyal"
style="width:24px;
height:24px"></span>
</div>
<div>
<p class="fw-bold mb-1">{{ _("Expected Revenue") }}</p>
<h4 class="fw-bolder text-nowrap">{{ opportunity.expected_revenue }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="px-xl-4 mb-7">
<div class="row mx-0 mx-sm-3 mx-lg-0 px-lg-0">
<div class="col-sm-12 col-xxl-6 border-bottom border-end-xxl border-translucent py-3">
<table class="w-100 table-stats table-stats">
<tr>
<th></th>
<th></th>
<th></th>
</tr>
<tr>
<td class="py-2">
<div class="d-inline-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3"
style="width:24px;
height:24px">
<span class="text-success-dark"
data-feather="bar-chart-2"
style="width:16px;
height:16px"></span>
</div>
<p class="fw-bold mb-0">{% trans "Probability (%)" %}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ opportunity.probability }} (%)</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0"
style="width:32px;
height:32px">
<span class="text-info-dark icon-saudi_riyal"
style="width:24px;
height:24px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Estimated Revenue") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">
<span class="icon-saudi_riyal"></span>{{ opportunity.expected_revenue }}
</p>
</td>
</tr>
</table>
</div>
<div class="col-sm-12 col-xxl-6 border-bottom border-translucent py-3">
<table class="w-100 table-stats">
<tr>

View File

@ -60,17 +60,6 @@
<span>{{ _("Phone Number") }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="contact"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2">
<span class="text-info-dark" data-feather="user"></span>
</div>
<span>{{ _("National ID") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="company"
@ -124,9 +113,6 @@
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">
<a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a>
</td>
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.national_id }}
</td>
<td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.address }}
</td>

View File

@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load humanize %}
{% load i18n static crispy_forms_filters custom_filters %}
{% block title %}
{{ _("View Customer") }}
@ -109,23 +110,50 @@
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-hover table-striped mb-0">
<thead class="bg-body-tertiary">
<table class="table fs-9 mb-0">
<thead>
<tr>
<th scope="col" style="width: 60%;">{% trans 'Note' %}</th>
<th scope="col" style="width: 15%;">{% trans 'Date' %}</th>
<th class="align-middle pe-6 text-uppercase text-start"
scope="col"
style="width:40%">{{ _("Note") }}</th>
<th class="align-middle text-start text-uppercase white-space-nowrap"
scope="col"
style="width:40%">{{ _("Created On") }}</th>
<th class="align-middle text-start text-uppercase white-space-nowrap"
scope="col"
style="width:40%">{{ _("Last Updated") }}</th>
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th>
</tr>
</thead>
<tbody>
<tbody id="notesTable">
{% for note in notes %}
<tr class="align-middle">
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-body-secondary">
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
{% if note.created_by == request.user %}
<a id="updateBtn"
href="#"
onclick="updateNote(this)"
class="btn btn-sm btn-phoenix-primary me-2"
data-pk="{{ note.pk }}"
data-note="{{ note.note|escapejs }}"
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
data-bs-toggle="modal"
data-bs-target="#noteModal"
data-note-title="{{ _("Update") }}">
<i class='fas fa-pen-square text-primary ms-2'></i>
{{ _("Update") }}
</a>
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'delete_note_to_lead' request.dealer.slug note.pk %}"
data-message="Are you sure you want to delete this note?"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -168,7 +168,6 @@
<h4 class="fw-bolder me-1">
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
</h4>
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
</div>
<ul class="list-unstyled mb-4">
{% for line in dealer.user.userplan.plan.description|splitlines %}
@ -222,7 +221,7 @@
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
</div>
</div>
<div class="mb-4">
@ -237,7 +236,7 @@
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ cars_count }}</span>
</div>
</div>
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>

View File

@ -50,7 +50,7 @@
<section id="how-to-request" class="policy-section">
<h2>{% trans "4. How to Request" %}</h2>
<p>
{% trans "Email our Billing and Support team at" %} <a href="mailto:haikal@support.sa">haikal@support.sa</a> {% trans "with your company name, account ID, invoice number, and a detailed reason for the refund." %}
{% trans "Email our Billing and Support team at" %} <a href="mailto:haikal@support.sa">haikal@tehnal.sa</a> {% trans "with your company name, account ID, invoice number, and a detailed reason for the refund." %}
</p>
</section>

View File

@ -259,7 +259,7 @@
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
<table class="table table-sm fs-9 mb-0 overflow-hidden">
{% if car.marked_price %}
{% if car.marked_price and request.is_accountant or request.is_dealer or request.is_manager %}
<tr>
<th>{% trans "Cost Price"|capfirst %}</th>
{% if request.is_dealer or request.is_accountant or request.manager%}

View File

@ -10,7 +10,6 @@
</a>
{% endblock %}
{% block content %}
{% if accounts or request.GET.q %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class="">
@ -199,10 +198,7 @@
</div>
</div>
</div>
{% else %}
{% url 'account_create' request.dealer.slug coa_pk as create_account_url %}
{% include "empty-illustration-page.html" with value="account" url=create_account_url %}
{% endif %}
{% endblock %}
{% block customerJS %}
<script>

View File

@ -18,7 +18,7 @@
<div class="fs-10 d-block">{{ task.scheduled_type|capfirst }}</div>
</td>
<td class="sent align-middle white-space-nowrap text-start fw-thin text-body-tertiary py-2">{{ task.notes }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{ task.scheduled_at|naturaltime|capfirst }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{ task.scheduled_at|naturaltime }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">
{% if task.completed %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>

View File

@ -315,7 +315,7 @@
class="form-control"
dir="ltr"
maxlength="10"
pattern="^05\d{8}$"
pattern="^(\+9665|05|9665)[0-9]{8}$"
inputmode="numeric"
placeholder='{{ _("05xxxxxxxx") }}'
value="{{ request.dealer.phone_number }}"
@ -668,7 +668,7 @@
function formatCardNumber(e) {
let val = this.value.replace(/\D/g, "").substring(0, 16);
this.value = val.replace(/(.{4})/g, "$1 ").trim();
// Validate as user types
validateCardNumber(this);
}

View File

@ -336,7 +336,7 @@
<form action="{% url 'update_estimate_additionals' request.dealer.slug estimate.pk %}"
method="post">
{% csrf_token %}
{{ additionals_form|crispy }}
{{ additionals_form|crispy }}
<button type="submit" class="btn btn-phoenix-primary">{% trans 'Update' %}</button>
</form>
</div>

View File

@ -141,7 +141,7 @@
<h3 class="h6 mb-3">
{{ plan.planpricing_set.first.price }}
<span class="icon-saudi_riyal"></span>
<span class="fs-8 fw-normal">/{{plan.planpricing_set.first.pricing.period}} {{ _("month") }}</span>
<span class="fs-8 fw-normal">/{{plan.planpricing_set.first.pricing.period}} {{ _("Days") }}</span>
</h3>
<h5 class="mb-3 h6">{{ _("Included") }}</h5>
<ul class="fa-ul ps-3 m-0">
@ -233,6 +233,6 @@
</div>
</div>
</section>
</div>
{% endblock %}