Compare commits

..

No commits in common. "e277ec5269d3289e8ef0428998c4e3b73f22425c" and "863aeab25c7253dd70f5f6b802db3ce85594d411" have entirely different histories.

12 changed files with 174 additions and 281 deletions

View File

@ -2133,8 +2133,7 @@ class VatRateForm(forms.ModelForm):
label="VAT Rate",
max_digits=5,
decimal_places=2,
validators=[vat_rate_validator],
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"),
validators=[vat_rate_validator]
)
class Meta:

View File

@ -9,8 +9,6 @@ def check_create_coa_accounts(task):
"""
Hook to verify account creation and handle failures
"""
from .models import Dealer
if task.success:
logger.info("Account creation task completed successfully")
return
@ -18,15 +16,14 @@ def check_create_coa_accounts(task):
logger.warning("Account creation task failed, checking status...")
try:
dealer_id = task.kwargs.get('dealer_id',None)
dealer_id = task.kwargs.get('dealer_id')
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:
logger.error("No dealer_id in task kwargs")
return
instance = Dealer.objects.get(id=dealer_id)
from .models import Dealer
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
entity = instance.entity
if not entity:
@ -37,11 +34,11 @@ def check_create_coa_accounts(task):
try:
coa = entity.get_coa_model_qs().get(slug=coa_slug)
except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
logger.error(f"COA with slug {coa_slug} not found for entity {entity.id}: {e}")
else:
coa = entity.get_default_coa()
if not coa:
logger.error(f"No COA for entity {entity.pk}")
logger.error(f"No COA for entity {entity.id}")
return
# Check which accounts are missing and create them
@ -49,7 +46,7 @@ def check_create_coa_accounts(task):
missing_accounts = []
for account_data in get_accounts_data():
if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists():
if not entity.get_all_accounts().filter(code=account_data["code"]).exists():
missing_accounts.append(account_data)
logger.info(f"Missing account: {account_data['code']}")

View File

@ -658,12 +658,16 @@ class Car(Base):
CarMake,
models.DO_NOTHING,
db_column="id_car_make",
null=True,
blank=True,
verbose_name=_("Make"),
)
id_car_model = models.ForeignKey(
CarModel,
models.DO_NOTHING,
db_column="id_car_model",
null=True,
blank=True,
verbose_name=_("Model"),
)
year = models.IntegerField(verbose_name=_("Year"))

View File

@ -138,36 +138,50 @@ def create_car_location(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs):
if not created:
return
if created:
try:
# Use transaction to ensure atomicity
with transaction.atomic():
# Create entity
entity = models.EntityModel.create_entity(
name=instance.user.dealer.name,
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 not entity:
raise Exception("Entity creation failed")
if entity:
instance.entity = entity
instance.save(update_fields=['entity'])
# Create default COA
entity.create_chart_of_accounts(
assign_as_default=True,
commit=True,
coa_name=f"{entity.name}-COA"
# Create COA synchronously first
coa = entity.create_chart_of_accounts(
assign_as_default=True, commit=True,
coa_name=_(f"{entity_name}-COA")
)
logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.")
if coa:
# Create essential UOMs synchronously
for u in models.UnitOfMeasure.choices:
entity.create_uom(name=u[1], unit_abbr=u[0])
# 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.error(f"💥 Failed setup for dealer {instance.id}: {e}")
# Optional: schedule retry or alert
logger.error(f"Failed to create ledger entity for dealer {instance.id}: {e}")
# Schedule retry task
async_task(
func="inventory.tasks.retry_entity_creation",
dealer_id=instance.id,
retry_count=0
)
# Create Entity
# @receiver(post_save, sender=models.Dealer)
# def create_ledger_entity(sender, instance, created, **kwargs):
@ -1392,7 +1406,6 @@ def handle_user_registration(sender, instance, created, **kwargs):
if instance.is_created:
logger.info(f"User account created: {instance.email}, sending email")
# instance.create_account()
send_email(
settings.DEFAULT_FROM_EMAIL,
instance.email,
@ -1417,31 +1430,15 @@ def handle_user_registration(sender, instance, created, **kwargs):
@receiver(post_save, sender=ChartOfAccountModel)
def handle_chart_of_account(sender, instance, created, **kwargs):
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:
# Schedule async account creation AFTER commit
transaction.on_commit(
lambda: async_task(
"inventory.tasks.create_coa_accounts",
dealer_id=dealer.pk,
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,
ack_failure=True, # Ensure task failures are acknowledged
sync=False # Explicitly set to async
)
)
# 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

@ -18,7 +18,7 @@ from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives
# from .utils import get_accounts_data, create_account
from .utils import get_accounts_data, create_account
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission
@ -63,64 +63,70 @@ def create_settings(pk):
)
def create_coa_accounts(dealer_id,**kwargs):
def create_coa_accounts(dealer_id, **kwargs):
"""
Idempotent: Creates only missing default accounts.
Safe to retry. Returns True if all done.
Create COA accounts with retry logic and proper error handling
"""
from .models import Dealer
from .utils import get_accounts_data, create_account
try:
dealer = Dealer.objects.get(pk=dealer_id)
entity = dealer.entity
from .utils import create_account, get_accounts_data
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}")
instance = Dealer.objects.get(pk=dealer_id)
entity = instance.entity
if not entity:
logger.error(f"❌ No entity for dealer {dealer_id}")
logger.error(f"No entity found for dealer {dealer_id}")
return False
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}")
return False
else:
coa = entity.get_default_coa()
logger.info(f"Default COA found for entity {entity.pk}")
if not coa:
logger.error(f"No default COA for entity {entity.pk}")
logger.error(f"No COA found for entity {entity.pk}")
return False
# Get missing accounts
existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True))
accounts_to_create = [
acc for acc in get_accounts_data()
if acc["code"] not in existing_codes
]
logger.info("Creating default accounts")
accounts_created = 0
if not accounts_to_create:
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
# Create missing ones
logger.info(f"🔧 Creating {len(accounts_to_create)} missing accounts...")
success = True
for acc in accounts_to_create:
if not create_account(entity, coa, acc):
logger.warning(f"⚠️ Failed to create account: {acc['code']}")
success = False # don't fail task, just log
if success:
logger.info("✅ All missing accounts created successfully.")
else:
logger.warning("⚠️ Some accounts failed to create — check logs.")
return success # Django-Q will mark as failed if False
except Exception as e:
logger.error(f"💥 Task failed for dealer {dealer_id}: {e}")
raise # Let Django-Q handle retry if configured
logger.error(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
logger.info(f"Retrying in {retry_delay} seconds...")
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
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)
# )
return False
def retry_entity_creation(dealer_id, retry_count=0):
"""
@ -155,7 +161,7 @@ def retry_entity_creation(dealer_id, retry_count=0):
# Now trigger account creation
async_task(
"inventory.tasks.create_coa_accounts",
dealer_id=dealer_id,
dealer_id=dealer_id
)
except Exception as e:

View File

@ -1332,7 +1332,6 @@ urlpatterns = [
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
path('payment_results/', views.payment_result, name='payment_result'),
]

View File

@ -2388,36 +2388,45 @@ def get_accounts_data():
def create_account(entity, coa, account_data):
logger.info(f"Creating account: {account_data['code']}")
logger.info(f"COA: {coa}")
"""
Create account with proper validation and error handling
"""
try:
# Skip if exists
if coa.get_coa_accounts().filter(code=account_data["code"]).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"]
)
if existing_account:
logger.info(f"Account already exists: {account_data['code']}")
return True
logger.info(f"Account does not exist: {account_data['code']},creating...")
account = coa.create_account(
logger.info(f"Creating account: {account_data['code']}")
account = entity.create_account(
coa_model=coa,
code=account_data["code"],
name=account_data["name"],
role=account_data["role"],
balance_type=account_data["balance_type"],
balance_type=_(account_data["balance_type"]),
active=True,
)
logger.info(f"Created account: {account}")
logger.info(f"Successfully created account: {account_data['code']}")
if account:
account.role_default = account_data.get("default", False)
account.save(update_fields=['role_default'])
account.role_default = account_data["default"]
account.save()
logger.info(f"Successfully created account: {account_data['code']}")
return True
except IntegrityError:
return True # Already created by race condition
logger.warning(f"Account {account_data['code']} already exists (IntegrityError)")
return True
except Exception as e:
logger.error(f"❌ Error creating {account_data['code']}: {e}")
logger.error(f"Error creating account {account_data['code']}: {e}")
return False
return False
# def create_account(entity, coa, account_data):
# try:
# account = entity.create_account(

View File

@ -9867,8 +9867,7 @@ def payment_callback(request, dealer_slug):
payment_status = request.GET.get("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
if history.status == "paid":
return redirect('home')
if payment_status == "paid":
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
@ -11764,9 +11763,3 @@ class CarDealershipSignUpView(CreateView):
response = super().form_valid(form)
messages.success(self.request, _('Your request has been submitted. We will contact you soon.'))
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')

View File

@ -50,6 +50,8 @@ body {
<span id="unread-count-text" class="fw-bold me-1">{{ total_count}}</span> {% trans "Notifications" %}
</span>
<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">
<i class="fas fa-check-double me-2"></i>{% trans "Mark all read" %}
</a>

View File

@ -1,50 +0,0 @@
{% 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 %}

View File

@ -1,50 +0,0 @@
{% 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 %}

View File

@ -8,7 +8,7 @@
{% block customCSS %}
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
integrity="sha512-X..."
integrity="sha512-X... (Your actual integrity hash)"
crossorigin="anonymous"
referrerpolicy="no-referrer" />
<style>
@ -144,6 +144,7 @@
}
.summary-box {
background-color: #ffffff;
border: 1px solid var(--border-color);
border-radius: 0.75rem;
padding: 2rem;
@ -176,9 +177,9 @@
.fa-ul .fa-li {
left: -1em;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="container py-5" id="pricing_container">
<h1 class="text-center mb-5 text-primary fw-bold">{{ _("Choose Your Plan") }}</h1>
@ -255,7 +256,6 @@
</div>
</div>
</div>
<div class="step d-none row justify-content-center" id="step2">
<div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("2. Enter Your Information") }}</h4>
@ -335,7 +335,6 @@
</div>
</div>
</div>
<div class="step d-none row justify-content-center" id="step3">
<div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("3. Payment Information") }}</h4>
@ -415,7 +414,6 @@
</div>
</div>
</div>
<div class="step d-none row justify-content-center" id="step4">
<div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("4. Confirm Your Information") }}</h4>
@ -425,10 +423,10 @@
<p class="mb-1"><i class="fas fa-box me-2"></i><strong>{{ _("Plan") }}:</strong> <span id="summary_plan"></span></p>
</div>
<div class="summary-item">
<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>
<p class="mb-1"><i class="fas fa-tag me-2"></i><strong>{{ _("Price") }}:</strong> <span id="summary_price"></span></p>
</div>
<div class="summary-item">
<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>
<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>
</div>
<hr class="my-3">
<div class="summary-item">
@ -460,7 +458,6 @@
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-5">
<button type="button"
class="btn btn-lg btn-phoenix-secondary px-5"
@ -479,6 +476,7 @@
{% block customJS %}
<script>
// wizard-form.js
document.addEventListener('DOMContentLoaded', initWizardForm);
document.addEventListener('htmx:afterSwap', initWizardForm);
@ -582,7 +580,6 @@
nextBtn.removeEventListener("click", handleNext);
prevBtn.removeEventListener("click", handlePrev);
// Add new listeners
nextBtn.addEventListener("click", handleNext);
prevBtn.addEventListener("click", handlePrev);
@ -625,7 +622,11 @@
}
function populateSummary() {
updatePricingSummary(); // Reuse logic for pricing
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
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 lastName = document.getElementById("last_name")?.value || '';
@ -696,20 +697,6 @@
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() {
// Add input event listeners for validation
const cardNameInput = document.getElementById("card_name");