more updates

This commit is contained in:
ismail 2025-09-09 18:52:20 +03:00
parent 53ea729530
commit f867f92562
29 changed files with 2063 additions and 1303 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,12 +2118,39 @@ 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(
max_digits=5,
decimal_places=2,
min_value=0,
max_value=100,
label=_("VAT Rate (%)"),
help_text=_("Enter VAT rate as percentage (e.g., 0.15 for 15%)")
)
class Meta:
model = VatRate
fields = ["rate"]
def clean_rate(self):
rate = self.cleaned_data['rate']
return rate / 100
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['rate'].initial = self.instance.rate * 100
class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(

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

@ -2338,8 +2338,17 @@ class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
def dealer_vat_rate_update(request, slug):
dealer = get_object_or_404(models.Dealer, slug=slug)
models.VatRate.objects.filter(dealer=dealer).update(rate=request.POST.get("rate"))
messages.success(request, _("VAT rate updated successfully"))
vat_rate, created = models.VatRate.objects.get_or_create(dealer=dealer)
form = forms.VatRateForm(request.POST, instance=vat_rate)
if form.is_valid():
form.save()
messages.success(request, _("VAT rate updated successfully"))
else:
logger.error(form.errors)
messages.error(request, _("VAT rate update failed: %s") % form.errors.as_text())
return redirect("dealer_detail", slug=slug)
@ -3629,17 +3638,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,
_(
@ -4190,7 +4199,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,
@ -4285,7 +4294,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,
@ -5915,6 +5924,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
@ -9801,7 +9813,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()
@ -9878,7 +9890,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:
@ -9914,7 +9926,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"
@ -10609,7 +10630,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,
@ -11465,15 +11488,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

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

@ -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>
<td>{{ car.cost_price|floatformat:2 }}</td>

View File

@ -223,7 +223,7 @@
name="go_to_stats"
value="true"
class="btn btn-lg btn-phoenix-primary">
{% trans "Save and Go to Inventory" %}
{% trans "Save" %}
</button>
</div>
</div>

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