more updates
This commit is contained in:
parent
53ea729530
commit
f867f92562
@ -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}")
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
@ -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,
|
||||
|
||||
@ -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):
|
||||
|
||||
14
inventory/management/commands/update_site.py
Normal file
14
inventory/management/commands/update_site.py
Normal 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}'))
|
||||
@ -169,7 +169,7 @@ class DealerSlugMiddleware:
|
||||
"/ar/help_center/",
|
||||
"/en/help_center/",
|
||||
]
|
||||
|
||||
|
||||
if request.path in paths:
|
||||
return None
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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}")
|
||||
@ -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):
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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})
|
||||
|
||||
|
||||
@ -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
162
requirements.prod.txt
Normal 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
|
||||
BIN
staticfiles/images/customers/3GCNY9EF5LG275234.png
Normal file
BIN
staticfiles/images/customers/3GCNY9EF5LG275234.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -1,4 +1,4 @@
|
||||
{% extends "allauth/layouts/entrance.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load allauth %}
|
||||
{% block head_title %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user