Compare commits
5 Commits
863aeab25c
...
e277ec5269
| Author | SHA1 | Date | |
|---|---|---|---|
| e277ec5269 | |||
| 255c08b74b | |||
| 62dd1188c9 | |||
| f2cd3f6969 | |||
| ad35058720 |
@ -2133,7 +2133,8 @@ class VatRateForm(forms.ModelForm):
|
|||||||
label="VAT Rate",
|
label="VAT Rate",
|
||||||
max_digits=5,
|
max_digits=5,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
validators=[vat_rate_validator]
|
validators=[vat_rate_validator],
|
||||||
|
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@ -9,6 +9,8 @@ def check_create_coa_accounts(task):
|
|||||||
"""
|
"""
|
||||||
Hook to verify account creation and handle failures
|
Hook to verify account creation and handle failures
|
||||||
"""
|
"""
|
||||||
|
from .models import Dealer
|
||||||
|
|
||||||
if task.success:
|
if task.success:
|
||||||
logger.info("Account creation task completed successfully")
|
logger.info("Account creation task completed successfully")
|
||||||
return
|
return
|
||||||
@ -16,14 +18,15 @@ def check_create_coa_accounts(task):
|
|||||||
logger.warning("Account creation task failed, checking status...")
|
logger.warning("Account creation task failed, checking status...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dealer_id = task.kwargs.get('dealer_id')
|
dealer_id = task.kwargs.get('dealer_id',None)
|
||||||
coa_slug = task.kwargs.get('coa_slug', None)
|
coa_slug = task.kwargs.get('coa_slug', None)
|
||||||
|
logger.info(f"Checking accounts for dealer {dealer_id}")
|
||||||
|
logger.info(f"COA slug: {coa_slug}")
|
||||||
if not dealer_id:
|
if not dealer_id:
|
||||||
logger.error("No dealer_id in task kwargs")
|
logger.error("No dealer_id in task kwargs")
|
||||||
return
|
return
|
||||||
|
|
||||||
from .models import Dealer
|
instance = Dealer.objects.get(id=dealer_id)
|
||||||
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
|
|
||||||
entity = instance.entity
|
entity = instance.entity
|
||||||
|
|
||||||
if not entity:
|
if not entity:
|
||||||
@ -34,11 +37,11 @@ def check_create_coa_accounts(task):
|
|||||||
try:
|
try:
|
||||||
coa = entity.get_coa_model_qs().get(slug=coa_slug)
|
coa = entity.get_coa_model_qs().get(slug=coa_slug)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"COA with slug {coa_slug} not found for entity {entity.id}: {e}")
|
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
|
||||||
else:
|
else:
|
||||||
coa = entity.get_default_coa()
|
coa = entity.get_default_coa()
|
||||||
if not coa:
|
if not coa:
|
||||||
logger.error(f"No COA for entity {entity.id}")
|
logger.error(f"No COA for entity {entity.pk}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check which accounts are missing and create them
|
# Check which accounts are missing and create them
|
||||||
@ -46,7 +49,7 @@ def check_create_coa_accounts(task):
|
|||||||
|
|
||||||
missing_accounts = []
|
missing_accounts = []
|
||||||
for account_data in get_accounts_data():
|
for account_data in get_accounts_data():
|
||||||
if not entity.get_all_accounts().filter(code=account_data["code"]).exists():
|
if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists():
|
||||||
missing_accounts.append(account_data)
|
missing_accounts.append(account_data)
|
||||||
logger.info(f"Missing account: {account_data['code']}")
|
logger.info(f"Missing account: {account_data['code']}")
|
||||||
|
|
||||||
|
|||||||
@ -658,16 +658,12 @@ class Car(Base):
|
|||||||
CarMake,
|
CarMake,
|
||||||
models.DO_NOTHING,
|
models.DO_NOTHING,
|
||||||
db_column="id_car_make",
|
db_column="id_car_make",
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Make"),
|
verbose_name=_("Make"),
|
||||||
)
|
)
|
||||||
id_car_model = models.ForeignKey(
|
id_car_model = models.ForeignKey(
|
||||||
CarModel,
|
CarModel,
|
||||||
models.DO_NOTHING,
|
models.DO_NOTHING,
|
||||||
db_column="id_car_model",
|
db_column="id_car_model",
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Model"),
|
verbose_name=_("Model"),
|
||||||
)
|
)
|
||||||
year = models.IntegerField(verbose_name=_("Year"))
|
year = models.IntegerField(verbose_name=_("Year"))
|
||||||
|
|||||||
@ -138,50 +138,36 @@ def create_car_location(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
@receiver(post_save, sender=models.Dealer)
|
@receiver(post_save, sender=models.Dealer)
|
||||||
def create_ledger_entity(sender, instance, created, **kwargs):
|
def create_ledger_entity(sender, instance, created, **kwargs):
|
||||||
if created:
|
if not created:
|
||||||
try:
|
return
|
||||||
# Use transaction to ensure atomicity
|
|
||||||
with transaction.atomic():
|
|
||||||
entity_name = instance.user.dealer.name
|
|
||||||
entity = EntityModel.create_entity(
|
|
||||||
name=entity_name,
|
|
||||||
admin=instance.user,
|
|
||||||
use_accrual_method=True,
|
|
||||||
fy_start_month=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if entity:
|
try:
|
||||||
instance.entity = entity
|
with transaction.atomic():
|
||||||
instance.save(update_fields=['entity'])
|
# Create entity
|
||||||
|
entity = models.EntityModel.create_entity(
|
||||||
|
name=instance.user.dealer.name,
|
||||||
|
admin=instance.user,
|
||||||
|
use_accrual_method=True,
|
||||||
|
fy_start_month=1,
|
||||||
|
)
|
||||||
|
if not entity:
|
||||||
|
raise Exception("Entity creation failed")
|
||||||
|
|
||||||
# Create COA synchronously first
|
instance.entity = entity
|
||||||
coa = entity.create_chart_of_accounts(
|
instance.save(update_fields=['entity'])
|
||||||
assign_as_default=True, commit=True,
|
|
||||||
coa_name=_(f"{entity_name}-COA")
|
|
||||||
)
|
|
||||||
|
|
||||||
if coa:
|
# Create default COA
|
||||||
# Create essential UOMs synchronously
|
entity.create_chart_of_accounts(
|
||||||
for u in models.UnitOfMeasure.choices:
|
assign_as_default=True,
|
||||||
entity.create_uom(name=u[1], unit_abbr=u[0])
|
commit=True,
|
||||||
|
coa_name=f"{entity.name}-COA"
|
||||||
# Schedule async task after successful synchronous operations
|
|
||||||
async_task(
|
|
||||||
func="inventory.tasks.create_coa_accounts",
|
|
||||||
dealer_id=instance.id, # Pass ID instead of object
|
|
||||||
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.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.")
|
||||||
logger.error(f"Failed to create ledger entity for dealer {instance.id}: {e}")
|
|
||||||
# Schedule retry task
|
except Exception as e:
|
||||||
async_task(
|
logger.error(f"💥 Failed setup for dealer {instance.id}: {e}")
|
||||||
func="inventory.tasks.retry_entity_creation",
|
# Optional: schedule retry or alert
|
||||||
dealer_id=instance.id,
|
|
||||||
retry_count=0
|
|
||||||
)
|
|
||||||
# Create Entity
|
# Create Entity
|
||||||
# @receiver(post_save, sender=models.Dealer)
|
# @receiver(post_save, sender=models.Dealer)
|
||||||
# def create_ledger_entity(sender, instance, created, **kwargs):
|
# def create_ledger_entity(sender, instance, created, **kwargs):
|
||||||
@ -1406,6 +1392,7 @@ def handle_user_registration(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if instance.is_created:
|
if instance.is_created:
|
||||||
logger.info(f"User account created: {instance.email}, sending email")
|
logger.info(f"User account created: {instance.email}, sending email")
|
||||||
|
# instance.create_account()
|
||||||
send_email(
|
send_email(
|
||||||
settings.DEFAULT_FROM_EMAIL,
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
instance.email,
|
instance.email,
|
||||||
@ -1430,15 +1417,31 @@ def handle_user_registration(sender, instance, created, **kwargs):
|
|||||||
@receiver(post_save, sender=ChartOfAccountModel)
|
@receiver(post_save, sender=ChartOfAccountModel)
|
||||||
def handle_chart_of_account(sender, instance, created, **kwargs):
|
def handle_chart_of_account(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
|
entity = instance.entity
|
||||||
|
dealer = instance.entity.admin.dealer
|
||||||
|
# Create UOMs (minimal, no logging per item)
|
||||||
|
if not entity.get_uom_all():
|
||||||
|
for code, name in models.UnitOfMeasure.choices:
|
||||||
|
entity.create_uom(name=name, unit_abbr=code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dealer = instance.entity.dealers.first()
|
# Schedule async account creation AFTER commit
|
||||||
async_task(
|
transaction.on_commit(
|
||||||
func="inventory.tasks.create_coa_accounts",
|
lambda: async_task(
|
||||||
dealer_id=dealer.pk, # Pass ID instead of object
|
"inventory.tasks.create_coa_accounts",
|
||||||
coa_slug=instance.slug,
|
dealer_id=dealer.pk,
|
||||||
hook="inventory.hooks.check_create_coa_accounts",
|
coa_slug=instance.slug,
|
||||||
ack_failure=True, # Ensure task failures are acknowledged
|
hook="inventory.hooks.check_create_coa_accounts",
|
||||||
sync=False # Explicitly set to async
|
ack_failure=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
# 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:
|
except Exception as e:
|
||||||
logger.error(f"Error handling chart of account: {e}")
|
logger.error(f"Error handling chart of account: {e}")
|
||||||
@ -18,7 +18,7 @@ from django.core.files.base import ContentFile
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from .utils import get_accounts_data, create_account
|
# from .utils import get_accounts_data, create_account
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
@ -63,70 +63,64 @@ def create_settings(pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_coa_accounts(dealer_id, **kwargs):
|
def create_coa_accounts(dealer_id,**kwargs):
|
||||||
"""
|
"""
|
||||||
Create COA accounts with retry logic and proper error handling
|
Idempotent: Creates only missing default accounts.
|
||||||
|
Safe to retry. Returns True if all done.
|
||||||
"""
|
"""
|
||||||
from .models import Dealer
|
from .models import Dealer
|
||||||
from .utils import create_account, get_accounts_data
|
from .utils import get_accounts_data, create_account
|
||||||
|
try:
|
||||||
|
dealer = Dealer.objects.get(pk=dealer_id)
|
||||||
|
entity = dealer.entity
|
||||||
|
coa_slug = kwargs.get('coa_slug', None)
|
||||||
|
if not entity:
|
||||||
|
logger.error(f"❌ No entity for dealer {dealer_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
max_retries = 3
|
if coa_slug:
|
||||||
retry_delay = 2 # seconds
|
try:
|
||||||
coa_slug = kwargs.get('coa_slug', None)
|
coa = entity.get_coa_model_qs().get(slug=coa_slug)
|
||||||
logger.info(f"chart of account model slug {coa_slug}")
|
except Exception as e:
|
||||||
logger.info(f"Attempting to create accounts for dealer {dealer_id}")
|
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
logger.info(f"Attempt {attempt + 1} to create accounts for dealer {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
|
return False
|
||||||
|
else:
|
||||||
|
coa = entity.get_default_coa()
|
||||||
|
|
||||||
if coa_slug:
|
if not coa:
|
||||||
try:
|
logger.error(f"❌ No default COA for entity {entity.pk}")
|
||||||
coa = entity.get_coa_model_qs().get(slug=coa_slug)
|
return False
|
||||||
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:
|
# Get missing accounts
|
||||||
logger.error(f"No COA found for entity {entity.pk}")
|
existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True))
|
||||||
return False
|
accounts_to_create = [
|
||||||
|
acc for acc in get_accounts_data()
|
||||||
|
if acc["code"] not in existing_codes
|
||||||
|
]
|
||||||
|
|
||||||
logger.info("Creating default accounts")
|
if not accounts_to_create:
|
||||||
accounts_created = 0
|
logger.info("✅ All default accounts already exist.")
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
for account_data in get_accounts_data():
|
|
||||||
logger.info(f"Creating account: {account_data['code']}")
|
|
||||||
if create_account(entity, coa, account_data):
|
|
||||||
accounts_created += 1
|
|
||||||
|
|
||||||
logger.info(f"Successfully created {accounts_created} accounts")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
# Create missing ones
|
||||||
logger.error(f"Attempt {attempt + 1} failed: {e}")
|
logger.info(f"🔧 Creating {len(accounts_to_create)} missing accounts...")
|
||||||
|
success = True
|
||||||
|
|
||||||
if attempt < max_retries - 1:
|
for acc in accounts_to_create:
|
||||||
logger.info(f"Retrying in {retry_delay} seconds...")
|
if not create_account(entity, coa, acc):
|
||||||
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
|
logger.warning(f"⚠️ Failed to create account: {acc['code']}")
|
||||||
else:
|
success = False # don't fail task, just log
|
||||||
logger.error(f"All {max_retries} attempts failed for dealer {dealer_id}")
|
|
||||||
# Schedule a cleanup or notification task
|
if success:
|
||||||
# async_task(
|
logger.info("✅ All missing accounts created successfully.")
|
||||||
# "inventory.tasks.handle_account_creation_failure",
|
else:
|
||||||
# dealer_id=dealer_id,
|
logger.warning("⚠️ Some accounts failed to create — check logs.")
|
||||||
# error=str(e)
|
|
||||||
# )
|
return success # Django-Q will mark as failed if False
|
||||||
return False
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"💥 Task failed for dealer {dealer_id}: {e}")
|
||||||
|
raise # Let Django-Q handle retry if configured
|
||||||
|
|
||||||
def retry_entity_creation(dealer_id, retry_count=0):
|
def retry_entity_creation(dealer_id, retry_count=0):
|
||||||
"""
|
"""
|
||||||
@ -161,7 +155,7 @@ def retry_entity_creation(dealer_id, retry_count=0):
|
|||||||
# Now trigger account creation
|
# Now trigger account creation
|
||||||
async_task(
|
async_task(
|
||||||
"inventory.tasks.create_coa_accounts",
|
"inventory.tasks.create_coa_accounts",
|
||||||
dealer_id=dealer_id
|
dealer_id=dealer_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -1332,6 +1332,7 @@ urlpatterns = [
|
|||||||
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
|
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
|
||||||
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
|
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
|
||||||
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
|
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
|
||||||
|
path('payment_results/', views.payment_result, name='payment_result'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -2388,45 +2388,36 @@ def get_accounts_data():
|
|||||||
|
|
||||||
|
|
||||||
def create_account(entity, coa, account_data):
|
def create_account(entity, coa, account_data):
|
||||||
"""
|
logger.info(f"Creating account: {account_data['code']}")
|
||||||
Create account with proper validation and error handling
|
logger.info(f"COA: {coa}")
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# existing_account = AccountModel.objects.filter(coa_model=coa,code=account_data["code"])
|
# Skip if exists
|
||||||
existing_account = entity.get_all_accounts().filter(
|
if coa.get_coa_accounts().filter(code=account_data["code"]).exists():
|
||||||
coa_model=coa,
|
|
||||||
code=account_data["code"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing_account:
|
|
||||||
logger.info(f"Account already exists: {account_data['code']}")
|
logger.info(f"Account already exists: {account_data['code']}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.info(f"Creating account: {account_data['code']}")
|
logger.info(f"Account does not exist: {account_data['code']},creating...")
|
||||||
account = entity.create_account(
|
account = coa.create_account(
|
||||||
coa_model=coa,
|
|
||||||
code=account_data["code"],
|
code=account_data["code"],
|
||||||
name=account_data["name"],
|
name=account_data["name"],
|
||||||
role=account_data["role"],
|
role=account_data["role"],
|
||||||
balance_type=_(account_data["balance_type"]),
|
balance_type=account_data["balance_type"],
|
||||||
active=True,
|
active=True,
|
||||||
)
|
)
|
||||||
logger.info(f"Successfully created account: {account_data['code']}")
|
logger.info(f"Created account: {account}")
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
account.role_default = account_data["default"]
|
account.role_default = account_data.get("default", False)
|
||||||
account.save()
|
account.save(update_fields=['role_default'])
|
||||||
logger.info(f"Successfully created account: {account_data['code']}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
logger.warning(f"Account {account_data['code']} already exists (IntegrityError)")
|
return True # Already created by race condition
|
||||||
return True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating account {account_data['code']}: {e}")
|
logger.error(f"❌ Error creating {account_data['code']}: {e}")
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# def create_account(entity, coa, account_data):
|
# def create_account(entity, coa, account_data):
|
||||||
# try:
|
# try:
|
||||||
# account = entity.create_account(
|
# account = entity.create_account(
|
||||||
|
|||||||
@ -2336,28 +2336,28 @@ class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
from .forms import VatRateForm
|
from .forms import VatRateForm
|
||||||
@login_required
|
@login_required
|
||||||
def dealer_vat_rate_update(request, slug):
|
def dealer_vat_rate_update(request, slug):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=slug)
|
dealer = get_object_or_404(models.Dealer, slug=slug)
|
||||||
|
|
||||||
vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer)
|
vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
form = VatRateForm(request.POST, instance=vat_rate_instance)
|
form = VatRateForm(request.POST, instance=vat_rate_instance)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(request, _("VAT rate updated successfully"))
|
messages.success(request, _("VAT rate updated successfully"))
|
||||||
return redirect("dealer_detail", slug=slug)
|
return redirect("dealer_detail", slug=slug)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
messages.error(request, _("Please enter valid vat rate between 0 and 1."))
|
messages.error(request, _("Please enter valid vat rate between 0 and 1."))
|
||||||
redirect("dealer_detail", slug=slug)
|
redirect("dealer_detail", slug=slug)
|
||||||
|
|
||||||
return redirect("dealer_detail", slug=slug)
|
return redirect("dealer_detail", slug=slug)
|
||||||
|
|
||||||
|
|
||||||
class DealerUpdateView(
|
class DealerUpdateView(
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
||||||
@ -9867,7 +9867,8 @@ def payment_callback(request, dealer_slug):
|
|||||||
payment_status = request.GET.get("status")
|
payment_status = request.GET.get("status")
|
||||||
logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
|
logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
|
||||||
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
|
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
|
||||||
|
if history.status == "paid":
|
||||||
|
return redirect('home')
|
||||||
if payment_status == "paid":
|
if payment_status == "paid":
|
||||||
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
|
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
|
||||||
|
|
||||||
@ -10764,7 +10765,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["entity_slug"] = dealer.entity.slug
|
context["entity_slug"] = dealer.entity.slug
|
||||||
context["vendors"] = vendors
|
context["vendors"] = vendors
|
||||||
context["empty_state_value"] = _("purchase order")
|
context["empty_state_value"] = _("purchase order")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -11762,4 +11763,10 @@ class CarDealershipSignUpView(CreateView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
response = super().form_valid(form)
|
response = super().form_valid(form)
|
||||||
messages.success(self.request, _('Your request has been submitted. We will contact you soon.'))
|
messages.success(self.request, _('Your request has been submitted. We will contact you soon.'))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def payment_result(request):
|
||||||
|
s = request.GET.get("status")
|
||||||
|
if s == "success":
|
||||||
|
return render(request, 'plans/payment_success.html')
|
||||||
|
return render(request, 'plans/payment_failed.html')
|
||||||
|
|||||||
@ -50,8 +50,6 @@ body {
|
|||||||
<span id="unread-count-text" class="fw-bold me-1">{{ total_count}}</span> {% trans "Notifications" %}
|
<span id="unread-count-text" class="fw-bold me-1">{{ total_count}}</span> {% trans "Notifications" %}
|
||||||
</span>
|
</span>
|
||||||
<a href="{% url 'mark_all_notifications_as_read' %}"
|
<a href="{% url 'mark_all_notifications_as_read' %}"
|
||||||
hx-get="{% url 'mark_all_notifications_as_read' %}"
|
|
||||||
hx-swap="none"
|
|
||||||
class="btn btn-sm btn-outline-secondary rounded-pill fw-bold">
|
class="btn btn-sm btn-outline-secondary rounded-pill fw-bold">
|
||||||
<i class="fas fa-check-double me-2"></i>{% trans "Mark all read" %}
|
<i class="fas fa-check-double me-2"></i>{% trans "Mark all read" %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
50
templates/plans/payment_failed.html
Normal file
50
templates/plans/payment_failed.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
{% block content %}
|
||||||
|
<main class="main">
|
||||||
|
<section class="py-4">
|
||||||
|
<div class="container d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h3 mb-0 text-body-emphasis">{% trans "Payment Failed" %}</h1>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'home' %}">{% trans "Home" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Payment Failed" %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="d-flex align-items-center justify-content-center py-5">
|
||||||
|
<div class="container text-center" style="max-width: 32rem;">
|
||||||
|
<div class="card p-5 shadow-sm border-0 rounded-4" data-aos="fade-up">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-x-circle-fill text-danger" style="font-size: 5rem;"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-3 text-body-emphasis fw-bold">{% trans "Payment Failed" %}</h2>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<p class="lead text-body-secondary">{{ message }}.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="lead text-body-secondary">
|
||||||
|
{% trans "We couldn't process your payment. Please review your information and try again." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-grid gap-3 col-md-8 mx-auto mt-4">
|
||||||
|
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-danger btn-lg rounded-pill">
|
||||||
|
{% trans "Go Back and Try Again" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-link text-body-secondary text-decoration-none">
|
||||||
|
{% trans "Back to Home" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock content %}
|
||||||
50
templates/plans/payment_success.html
Normal file
50
templates/plans/payment_success.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="main">
|
||||||
|
<section class="py-4">
|
||||||
|
<div class="container d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h3 mb-0 text-body-emphasis">{% trans "Payment Successful" %}</h1>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb mb-0">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'home' %}">{% trans "Home" %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{% trans "Success" %}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="d-flex align-items-center justify-content-center py-5">
|
||||||
|
<div class="container text-center" style="max-width: 32rem;">
|
||||||
|
<div class="card p-5 shadow-sm border-0 rounded-4" data-aos="fade-up">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-check-circle-fill text-success" style="font-size: 5rem;"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-4 mb-3 text-body-emphasis fw-bold">{{ _("Thank You") }}!</h2>
|
||||||
|
|
||||||
|
<p class="lead text-body-secondary">
|
||||||
|
{{ _("Your payment was successful") }}. {{ _("Your order is being processed") }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-grid gap-3 col-md-8 mx-auto mt-4">
|
||||||
|
{% if invoice %}
|
||||||
|
<a href="{% url 'invoice_preview_html' invoice.pk %}"
|
||||||
|
class="btn btn-primary btn-lg rounded-pill">
|
||||||
|
<i class="fas fa-eye me-2"></i> {{ _("View Invoice") }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-link text-body-secondary text-decoration-none">
|
||||||
|
<i class="fas fa-home me-2"></i> {{ _("Back to Home") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock content %}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
{% block customCSS %}
|
{% block customCSS %}
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
||||||
integrity="sha512-X... (Your actual integrity hash)"
|
integrity="sha512-X..."
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
referrerpolicy="no-referrer" />
|
referrerpolicy="no-referrer" />
|
||||||
<style>
|
<style>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
--card-selected-shadow: rgba(13, 110, 253, 0.4);
|
--card-selected-shadow: rgba(13, 110, 253, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.pricing-card-label {
|
.pricing-card-label {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -144,7 +144,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-box {
|
.summary-box {
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@ -177,9 +176,9 @@
|
|||||||
.fa-ul .fa-li {
|
.fa-ul .fa-li {
|
||||||
left: -1em;
|
left: -1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock customCSS %}
|
{% endblock customCSS %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container py-5" id="pricing_container">
|
<div class="container py-5" id="pricing_container">
|
||||||
<h1 class="text-center mb-5 text-primary fw-bold">{{ _("Choose Your Plan") }}</h1>
|
<h1 class="text-center mb-5 text-primary fw-bold">{{ _("Choose Your Plan") }}</h1>
|
||||||
@ -256,6 +255,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step d-none row justify-content-center" id="step2">
|
<div class="step d-none row justify-content-center" id="step2">
|
||||||
<div class="col-lg-8 col-md-10">
|
<div class="col-lg-8 col-md-10">
|
||||||
<h4 class="text-center mb-4 text-secondary">{{ _("2. Enter Your Information") }}</h4>
|
<h4 class="text-center mb-4 text-secondary">{{ _("2. Enter Your Information") }}</h4>
|
||||||
@ -335,6 +335,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step d-none row justify-content-center" id="step3">
|
<div class="step d-none row justify-content-center" id="step3">
|
||||||
<div class="col-lg-8 col-md-10">
|
<div class="col-lg-8 col-md-10">
|
||||||
<h4 class="text-center mb-4 text-secondary">{{ _("3. Payment Information") }}</h4>
|
<h4 class="text-center mb-4 text-secondary">{{ _("3. Payment Information") }}</h4>
|
||||||
@ -414,6 +415,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step d-none row justify-content-center" id="step4">
|
<div class="step d-none row justify-content-center" id="step4">
|
||||||
<div class="col-lg-8 col-md-10">
|
<div class="col-lg-8 col-md-10">
|
||||||
<h4 class="text-center mb-4 text-secondary">{{ _("4. Confirm Your Information") }}</h4>
|
<h4 class="text-center mb-4 text-secondary">{{ _("4. Confirm Your Information") }}</h4>
|
||||||
@ -423,10 +425,10 @@
|
|||||||
<p class="mb-1"><i class="fas fa-box me-2"></i><strong>{{ _("Plan") }}:</strong> <span id="summary_plan"></span></p>
|
<p class="mb-1"><i class="fas fa-box me-2"></i><strong>{{ _("Plan") }}:</strong> <span id="summary_plan"></span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<p class="mb-1"><i class="fas fa-tag me-2"></i><strong>{{ _("Price") }}:</strong> <span id="summary_price"></span></p>
|
<p class="mb-1"><i class="fas fa-tag me-2"></i><strong>{{ _("Price (excl. VAT)") }}:</strong> <span id="summary_price"></span> <span class="icon-saudi_riyal"></span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<p class="mb-1"><i class="fas fa-receipt me-2"></i><strong>{{ _("VAT") }} (15%):</strong> <span id="summary-tax">0.00</span> <span class="icon-saudi_riyal"></span></p>
|
<p class="mb-1"><i class="fas fa-receipt me-2"></i><strong>{{ _("VAT") }} (15%):</strong> <span id="summary-tax"></span> <span class="icon-saudi_riyal"></span></p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
@ -458,6 +460,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-5">
|
<div class="d-flex justify-content-between mt-5">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-lg btn-phoenix-secondary px-5"
|
class="btn btn-lg btn-phoenix-secondary px-5"
|
||||||
@ -472,11 +475,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
// wizard-form.js
|
|
||||||
document.addEventListener('DOMContentLoaded', initWizardForm);
|
document.addEventListener('DOMContentLoaded', initWizardForm);
|
||||||
document.addEventListener('htmx:afterSwap', initWizardForm);
|
document.addEventListener('htmx:afterSwap', initWizardForm);
|
||||||
|
|
||||||
@ -484,22 +486,22 @@
|
|||||||
const form = document.getElementById('wizardForm');
|
const form = document.getElementById('wizardForm');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
||||||
// Remove old event listeners to prevent duplicates
|
// Remove old event listeners to prevent duplicates
|
||||||
form.removeEventListener('submit', handleFormSubmit);
|
form.removeEventListener('submit', handleFormSubmit);
|
||||||
|
|
||||||
// Add new submit handler
|
// Add new submit handler
|
||||||
form.addEventListener('submit', handleFormSubmit);
|
form.addEventListener('submit', handleFormSubmit);
|
||||||
|
|
||||||
// Initialize radio button selections
|
// Initialize radio button selections
|
||||||
initPlanSelection();
|
initPlanSelection();
|
||||||
|
|
||||||
// Initialize wizard steps
|
// Initialize wizard steps
|
||||||
initWizardSteps();
|
initWizardSteps();
|
||||||
|
|
||||||
// Initialize card input formatting
|
// Initialize card input formatting
|
||||||
initCardInputs();
|
initCardInputs();
|
||||||
|
|
||||||
// Initialize summary updates
|
// Initialize summary updates
|
||||||
initSummaryUpdates();
|
initSummaryUpdates();
|
||||||
|
|
||||||
// Initialize validation
|
// Initialize validation
|
||||||
@ -523,7 +525,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit the form after a slight delay
|
// Submit the form after a slight delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
if (form.reportValidity()) {
|
if (form.reportValidity()) {
|
||||||
@ -537,13 +539,13 @@
|
|||||||
function initPlanSelection() {
|
function initPlanSelection() {
|
||||||
const radios = document.querySelectorAll('.btn-check');
|
const radios = document.querySelectorAll('.btn-check');
|
||||||
radios.forEach(radio => {
|
radios.forEach(radio => {
|
||||||
// Remove old listeners
|
// Remove old listeners
|
||||||
radio.removeEventListener('change', handlePlanChange);
|
radio.removeEventListener('change', handlePlanChange);
|
||||||
|
|
||||||
// Add new listeners
|
// Add new listeners
|
||||||
radio.addEventListener('change', handlePlanChange);
|
radio.addEventListener('change', handlePlanChange);
|
||||||
|
|
||||||
// Initialize selected state
|
// Initialize selected state
|
||||||
if (radio.checked) {
|
if (radio.checked) {
|
||||||
updatePlanSelection(radio.id);
|
updatePlanSelection(radio.id);
|
||||||
}
|
}
|
||||||
@ -580,8 +582,9 @@
|
|||||||
nextBtn.removeEventListener("click", handleNext);
|
nextBtn.removeEventListener("click", handleNext);
|
||||||
prevBtn.removeEventListener("click", handlePrev);
|
prevBtn.removeEventListener("click", handlePrev);
|
||||||
|
|
||||||
nextBtn.addEventListener("click", handleNext);
|
// Add new listeners
|
||||||
prevBtn.addEventListener("click", handlePrev);
|
nextBtn.addEventListener("click", handleNext);
|
||||||
|
prevBtn.addEventListener("click", handlePrev);
|
||||||
|
|
||||||
function showStep(index) {
|
function showStep(index) {
|
||||||
steps.forEach((step, i) => {
|
steps.forEach((step, i) => {
|
||||||
@ -622,11 +625,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populateSummary() {
|
function populateSummary() {
|
||||||
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
|
updatePricingSummary(); // Reuse logic for pricing
|
||||||
if (selectedPlan) {
|
|
||||||
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
|
|
||||||
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstName = document.getElementById("first_name")?.value || '';
|
const firstName = document.getElementById("first_name")?.value || '';
|
||||||
const lastName = document.getElementById("last_name")?.value || '';
|
const lastName = document.getElementById("last_name")?.value || '';
|
||||||
@ -648,7 +647,7 @@
|
|||||||
return "**** **** **** " + last4;
|
return "**** **** **** " + last4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
showStep(currentStep);
|
showStep(currentStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,7 +668,7 @@
|
|||||||
function formatCardNumber(e) {
|
function formatCardNumber(e) {
|
||||||
let val = this.value.replace(/\D/g, "").substring(0, 16);
|
let val = this.value.replace(/\D/g, "").substring(0, 16);
|
||||||
this.value = val.replace(/(.{4})/g, "$1 ").trim();
|
this.value = val.replace(/(.{4})/g, "$1 ").trim();
|
||||||
|
|
||||||
// Validate as user types
|
// Validate as user types
|
||||||
validateCardNumber(this);
|
validateCardNumber(this);
|
||||||
}
|
}
|
||||||
@ -697,6 +696,20 @@
|
|||||||
updatePricingSummary(); // Initial call
|
updatePricingSummary(); // Initial call
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePricingSummary() {
|
||||||
|
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
|
||||||
|
if (!selectedPlan) return;
|
||||||
|
|
||||||
|
const priceWithTax = parseFloat(selectedPlan.dataset.price);
|
||||||
|
const basePrice = priceWithTax / 1.15;
|
||||||
|
const vat = priceWithTax - basePrice;
|
||||||
|
|
||||||
|
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
|
||||||
|
document.getElementById("summary_price").textContent = basePrice.toFixed(2);
|
||||||
|
document.getElementById("summary-tax").textContent = vat.toFixed(2);
|
||||||
|
document.getElementById("summary-total").textContent = priceWithTax.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
function initValidation() {
|
function initValidation() {
|
||||||
// Add input event listeners for validation
|
// Add input event listeners for validation
|
||||||
const cardNameInput = document.getElementById("card_name");
|
const cardNameInput = document.getElementById("card_name");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user