implement payment with moyasar

This commit is contained in:
ismail 2025-05-01 17:03:58 +03:00
parent c01d234e0e
commit 239ea2e66e
20 changed files with 1581 additions and 384 deletions

2
.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"request": "launch",
"args": [
"runserver",
"0.0.0.0:8888"
"0.0.0.0:8000"
],
"django": true,
"autoStartBrowser": false,

View File

@ -1,9 +1,13 @@
from django.core.cache import cache
from datetime import datetime
from luhnchecker.luhn import Luhn
from django.core.validators import RegexValidator
from django.contrib.auth.models import Permission
from appointment.models import Service
from phonenumber_field.formfields import PhoneNumberField
from django.core.validators import MinLengthValidator
from django import forms
from plans.models import PlanPricing
from django.contrib.auth import get_user_model
from .models import CustomGroup, Status, Stage
from .mixins import AddClassMixin
@ -462,6 +466,7 @@ class VendorForm(forms.ModelForm):
:ivar Meta: Inner class to define metadata for the Vendor form.
:type Meta: Type[VendorForm.Meta]
"""
class Meta:
model = Vendor
fields = [
@ -1427,3 +1432,216 @@ class JournalEntryModelCreateForm(JournalEntryModelCreateFormBase):
:type bar: int
"""
pass
class PlanPricingForm(forms.ModelForm):
"""
Represents a form for managing plan pricing.
This class provides a form for creating or updating `PlanPricing` instances.
It automatically includes all fields defined in the `PlanPricing` model and
can be used within a Django web application.
:ivar model: Associated model for the form.
:type model: PlanPricing
:ivar fields: Fields to include in the form. The value "__all__" indicates
that all model fields should be included.
:type fields: str
"""
class Meta:
model = PlanPricing
fields = ["plan","pricing", "price"]
class CreditCardField(forms.CharField):
def clean(self, value):
value = super().clean(value)
if value:
# Remove all non-digit characters
cleaned_value = ''.join(c for c in value if c.isdigit())
# Validate using Luhn algorithm
if not Luhn.check_luhn(cleaned_value):
raise forms.ValidationError("Please enter a valid credit card number")
# Add basic card type detection (optional)
if cleaned_value.startswith('4'):
self.card_type = 'visa'
elif cleaned_value.startswith(('51', '52', '53', '54', '55')):
self.card_type = 'mastercard'
elif cleaned_value.startswith(('34', '37')):
self.card_type = 'amex'
else:
self.card_type = 'unknown'
return value
return value
class ExpiryDateField(forms.CharField):
def clean(self, value):
value = super().clean(value)
if value:
try:
month, year = value.split('/')
month = int(month.strip())
year = int(year.strip())
# Handle 2-digit year
if year < 100:
year += 2000
# Validate month
if month < 1 or month > 12:
raise forms.ValidationError("Please enter a valid month (01-12)")
# Validate not expired
current_year = datetime.now().year
current_month = datetime.now().month
if year < current_year or (year == current_year and month < current_month):
raise forms.ValidationError("This card appears to be expired")
except (ValueError, AttributeError):
raise forms.ValidationError("Please enter a valid expiry date in MM/YY format")
return value
class CVVField(forms.CharField):
def clean(self, value):
value = super().clean(value)
if value:
if not value.isdigit():
raise forms.ValidationError("CVV must contain only digits")
if len(value) not in (3, 4):
raise forms.ValidationError("CVV must be 3 or 4 digits")
return value
class PaymentPlanForm(forms.Form):
# Customer Information
first_name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Your First Name'),
'id': 'first-name'
}),
label=_('First Name')
)
last_name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Your Last Name'),
'id': 'last-name'
}),
label=_('Last Name')
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': _('Your Email'),
'id': 'email'
}),
label=_('Email Address')
)
phone = forms.CharField(
required=False,
max_length=20,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Your Phone Number'),
'id': 'phone'
}),
label=_('Phone Number'),
validators=[RegexValidator(
regex=r'^\+?[0-9]{8,15}$',
message=_('Enter a valid phone number (8-15 digits, + optional)')
)]
)
# Credit Card Fields (not saved to database)
card_number = CreditCardField(
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '1234 5678 9012 3456',
'id': 'card-number',
}),
label="Card Number"
)
expiry_date = ExpiryDateField(
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'MM/YY',
'id': 'expiry',
}),
label="Expiration Date"
)
cvv = CVVField(
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '123',
'id': 'cvv',
}),
label="Security Code (CVV)"
)
card_name = forms.CharField(
required=True,
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'John Doe',
'id': 'card-name',
}),
label="Name on Card"
)
# Terms and conditions
terms = forms.BooleanField(
required=True,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
'id': 'terms'
}),
label=_('I agree to the Terms and Conditions'),
error_messages={
'required': _('You must accept the terms and conditions')
}
)
def clean(self):
cleaned_data = super().clean()
payment_method = self.data.get('payment-method') # From your radio buttons
if payment_method == 'credit-card':
if not all([
cleaned_data.get('card_number'),
cleaned_data.get('expiry_date'),
cleaned_data.get('cvv'),
cleaned_data.get('card_name')
]):
raise forms.ValidationError("Please complete all credit card fields")
return cleaned_data
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and user.is_authenticated:
# Pre-fill form with user data if available
self.fields['first_name'].initial = user.first_name
self.fields['last_name'].initial = user.last_name
self.fields['email'].initial = user.email

View File

@ -0,0 +1,18 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from plans.models import UserPlan
class Command(BaseCommand):
help = 'Deactivates expired user plans'
def handle(self, *args, **options):
expired_plans = UserPlan.objects.filter(
active=True,
expire__lt=timezone.now()
)
count = expired_plans.count()
for plan in expired_plans:
plan.expire_account()
self.stdout.write(self.style.SUCCESS(f'Successfully deactivated {count} expired plans'))

View File

@ -1,9 +1,12 @@
# management/commands/create_plans.py
from decimal import Decimal
from datetime import timedelta
from django.db.models import Q
from django.utils import timezone
from plans.quota import get_user_quota
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing
from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing,UserPlan,Order,BillingInfo,AbstractOrder
class Command(BaseCommand):
help = 'Create basic subscription plans structure'
@ -15,10 +18,14 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
Plan.objects.all().delete()
Quota.objects.all().delete()
PlanQuota.objects.all().delete()
Pricing.objects.all().delete()
# Plan.objects.all().delete()
# Quota.objects.all().delete()
# PlanQuota.objects.all().delete()
# Pricing.objects.all().delete()
# PlanPricing.objects.all().delete()
# UserPlan.objects.all().delete()
# Order.objects.all().delete()
# BillingInfo.objects.all().delete()
three_users_quota = Quota.objects.create(name='3 users', codename='3 users', unit='number')
five_users_quota = Quota.objects.create(name='5 users', codename='5 users', unit='number')
@ -38,10 +45,63 @@ class Command(BaseCommand):
# PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100)
# Define pricing
basic_pricing = Pricing.objects.create(plan=basic_plan, name='Monthly', period=30)
pro_pricing = Pricing.objects.create(plan=pro_plan, name='Monthly', period=30)
enterprise_pricing = Pricing.objects.create(plan=enterprise_plan, name='Monthly', period=30)
basic_pricing = Pricing.objects.create(name='Monthly', period=30)
pro_pricing = Pricing.objects.create(name='Monthly', period=30)
enterprise_pricing = Pricing.objects.create(name='Monthly', period=30)
PlanPricing.objects.create(plan=basic_plan, pricing=basic_pricing, price=Decimal('9.99'))
PlanPricing.objects.create(plan=pro_plan, pricing=pro_pricing, price=Decimal('19.99'))
PlanPricing.objects.create(plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal('29.99'))
PlanPricing.objects.create(plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal('29.99'))
# # Create quotas
# project_quota = Quota.objects.create(name='projects', codename='projects', unit='projects')
# storage_quota = Quota.objects.create(name='storage', codename='storage', unit='GB')
# # Create plans
# basic_plan = Plan.objects.create(name='Basic', description='Basic plan', available=True, visible=True)
# pro_plan = Plan.objects.create(name='Pro', description='Pro plan', available=True, visible=True)
# # Assign quotas to plans
# PlanQuota.objects.create(plan=basic_plan, quota=project_quota, value=5)
# PlanQuota.objects.create(plan=basic_plan, quota=storage_quota, value=10)
# PlanQuota.objects.create(plan=pro_plan, quota=project_quota, value=50)
# PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100)
# # Define pricing
# basic = Pricing.objects.create(name='Monthly', period=30)
# pro = Pricing.objects.create(name='Monthly', period=30)
# basic_pricing = PlanPricing.objects.create(plan=basic_plan, pricing=basic, price=Decimal('19.99'))
# pro_pricing = PlanPricing.objects.create(plan=pro_plan, pricing=pro, price=Decimal('29.99'))
# Create users
user = User.objects.first()
# # Create user plans
# billing_info = BillingInfo.objects.create(
# user=user,
# tax_number='123456789',
# name='John Doe',
# street='123 Main St',
# zipcode='12345',
# city='Anytown',
# country='US',
# )
# order = Order.objects.create(
# user=user,
# plan=pro_plan,
# pricing=pro_pricing,
# amount=pro_pricing.price,
# currency="SAR",
# )
UserPlan.objects.create(
user=user,
plan=pro_plan,
expire=timezone.now() + timedelta(days=2),
active=True,
)

View File

@ -0,0 +1,47 @@
# Generated by Django 5.1.7 on 2025-04-29 13:33
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_alter_email_object_id_alter_notes_object_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='PaymentHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_data', models.JSONField(blank=True, null=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0.01)])),
('currency', models.CharField(default='SAR', max_length=3)),
('payment_date', models.DateTimeField(default=django.utils.timezone.now)),
('status', models.CharField(choices=[('initiated', 'initiated'), ('pending', 'Pending'), ('completed', 'Completed'), ('paid', 'Paid'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
('payment_method', models.CharField(choices=[('credit_card', 'Credit Card'), ('debit_card', 'Debit Card'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('crypto', 'Cryptocurrency'), ('other', 'Other')], max_length=20)),
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('invoice_number', models.CharField(blank=True, max_length=50, null=True)),
('order_reference', models.CharField(blank=True, max_length=100, null=True)),
('gateway_response', models.JSONField(blank=True, null=True)),
('gateway_name', models.CharField(blank=True, max_length=50, null=True)),
('description', models.TextField(blank=True, null=True)),
('is_recurring', models.BooleanField(default=False)),
('billing_email', models.EmailField(blank=True, max_length=254, null=True)),
('billing_address', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'],
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-05-01 13:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_paymenthistory'),
]
operations = [
migrations.AlterField(
model_name='vendor',
name='address',
field=models.CharField(default='', max_length=200, verbose_name='Address'),
preserve_default=False,
),
]

View File

@ -1,5 +1,7 @@
from django.contrib.auth.models import Permission
from decimal import Decimal
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
import hashlib
from django.db import models
from datetime import timedelta
@ -24,6 +26,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from appointment.models import StaffMember
from plans.quota import get_user_quota
from plans.models import UserPlan,Quota,PlanQuota
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
@ -910,12 +913,28 @@ class Dealer(models.Model, LocalizedNameMixin):
objects = DealerUserManager()
# @property
# def get_active_plan(self):
# try:
# return self.user.subscription_set.filter(is_active=True).first()
# except SubscriptionPlan.DoesNotExist:
# return None
@property
def active_plan(self):
try:
return UserPlan.objects.get(user=self.user,active=True).plan
except Exception as e:
print(e)
return None
@property
def user_quota(self):
try:
return PlanQuota.objects.get(plan=self.active_plan).value
except Exception as e:
print(e)
return None
@property
def is_staff_exceed_quota_limit(self):
quota = self.user_quota
staff_count = self.staff.count()
if staff_count >= quota:
return True
return False
#
# @property
# def get_plan(self):
@ -1561,7 +1580,7 @@ class Vendor(models.Model, LocalizedNameMixin):
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
max_length=200, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
@ -1973,4 +1992,100 @@ class DealerSettings(models.Model):
# db_index=True,
# unique=False,
# null=True,
# )
# )
class PaymentHistory(models.Model):
# Payment status choices
INITIATED = "initiated"
PENDING = "pending"
PAID = "paid"
COMPLETED = "completed"
FAILED = "failed"
REFUNDED = "refunded"
CANCELLED = "cancelled"
PAYMENT_STATUS_CHOICES = [
(INITIATED, "initiated"),
(PENDING, "Pending"),
(COMPLETED, "Completed"),
(PAID, "Paid"),
(FAILED, "Failed"),
(REFUNDED, "Refunded"),
(CANCELLED, "Cancelled"),
]
# Payment method choices
CREDIT_CARD = "credit_card"
DEBIT_CARD = "debit_card"
PAYPAL = "paypal"
BANK_TRANSFER = "bank_transfer"
CRYPTO = "crypto"
OTHER = "other"
PAYMENT_METHOD_CHOICES = [
(CREDIT_CARD, "Credit Card"),
(DEBIT_CARD, "Debit Card"),
(PAYPAL, "PayPal"),
(BANK_TRANSFER, "Bank Transfer"),
(CRYPTO, "Cryptocurrency"),
(OTHER, "Other"),
]
# Basic payment information
user = models.ForeignKey(
"auth.User", # or your custom user model
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="payments",
)
user_data = models.JSONField(null=True, blank=True)
amount = models.DecimalField(
max_digits=10, decimal_places=2, validators=[MinValueValidator(0.01)]
)
currency = models.CharField(max_length=3, default="SAR")
payment_date = models.DateTimeField(default=timezone.now)
status = models.CharField(
max_length=10, choices=PAYMENT_STATUS_CHOICES, default=PENDING
)
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHOD_CHOICES)
# Transaction references
transaction_id = models.CharField(
max_length=100, unique=True, blank=True, null=True
)
invoice_number = models.CharField(max_length=50, blank=True, null=True)
order_reference = models.CharField(max_length=100, blank=True, null=True)
# Payment processor details
gateway_response = models.JSONField(
blank=True, null=True
) # Raw response from payment gateway
gateway_name = models.CharField(max_length=50, blank=True, null=True)
# Additional metadata
description = models.TextField(blank=True, null=True)
is_recurring = models.BooleanField(default=False)
billing_email = models.EmailField(blank=True, null=True)
billing_address = models.TextField(blank=True, null=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "Payment Histories"
ordering = ["-payment_date"]
indexes = [
models.Index(fields=["transaction_id"]),
models.Index(fields=["user"]),
models.Index(fields=["status"]),
models.Index(fields=["payment_date"]),
]
def __str__(self):
return f"Payment #{self.id} - {self.amount} {self.currency} ({self.status})"
def is_successful(self):
return self.status == self.COMPLETED

View File

@ -189,7 +189,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
:return: None
"""
if created:
entity = EntityModel.objects.filter(name=instance.dealer.name).first()
entity = EntityModel.objects.filter(admin=instance.dealer.user).first()
additionals = to_dict(instance)
vendor = entity.create_vendor(
vendor_model_kwargs={
@ -208,6 +208,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
coa = entity.get_default_coa()
last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first()
# code = f"{int(last_account.code)}{1:03d}"
if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}"

View File

@ -45,6 +45,10 @@ urlpatterns = [
# ),
# Dashboards
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
path("pricing/", views.pricing_page, name="pricing_page"),
path("submit_plan/", views.submit_plan, name="submit_plan"),
path('payment-callback/', views.payment_callback, name='payment_callback'),
#
path("dealers/<int:pk>/settings/", views.DealerSettingsView, name="dealer_settings"),
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"),

View File

@ -1,35 +1,31 @@
from django_ledger.io import roles
from django.core.exceptions import ObjectDoesNotExist
import json
import random
import datetime
from django.shortcuts import redirect
from django.contrib import messages
from django.utils import timezone
from django_ledger.models.entity import UnitOfMeasureModel
from django_ledger.models.journal_entry import JournalEntryModel
from django_ledger.models.ledger import LedgerModel
from django_ledger.models.transactions import TransactionModel
from plans.models import AbstractOrder
from django.contrib.auth.models import Group,Permission
from django.db import transaction
from django.urls import reverse
import requests
from decimal import Decimal
from django.utils import timezone
from django_ledger.io import roles
from django.contrib import messages
from django.shortcuts import redirect
from django.core.exceptions import ObjectDoesNotExist
from django_ledger.models.journal_entry import JournalEntryModel
from django_ledger.models.transactions import TransactionModel
from inventory import models
from django.conf import settings
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from inventory.utilities.financials import get_financial_value
from django_ledger.models.items import ItemModel
from django_ledger.models import (
InvoiceModel,
EstimateModel,
BillModel,
VendorModel,
CustomerModel,
ItemTransactionModel,
AccountModel
)
from decimal import Decimal
from django.utils.translation import get_language
from appointment.models import StaffMember
from django.contrib.auth.models import User
def get_jwt_token():
"""
@ -1262,4 +1258,108 @@ def create_make_accounts(dealer):
coa_model=coa,
balance_type="credit",
active=True
)
)
def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address):
with transaction.atomic():
user = User.objects.create(username=email, email=email)
user.set_password(password)
user.save()
group = Group.objects.create(name=f"{user.pk}-Admin")
user.groups.add(group)
for perm in Permission.objects.filter(
content_type__app_label__in=["inventory", "django_ledger"]
):
group.permissions.add(perm)
StaffMember.objects.create(user=user)
models.Dealer.objects.create(
user=user,
name=name,
arabic_name=arabic_name,
crn=crn,
vrn=vrn,
phone_number=phone,
address=address,
)
def handle_payment(request,order):
url = "https://api.moyasar.com/v1/payments"
callback_url = request.build_absolute_uri(reverse("payment_callback"))
if request.user.is_authenticated:
# email = request.user.email
# first_name = request.user.first_name
# last_name = request.user.last_name
# phone = request.user.phone
# else:
email = request.POST["email"]
first_name = request.POST["first_name"]
last_name = request.POST["last_name"]
phone = request.POST["phone"]
card_name = request.POST["card_name"]
card_number = str(request.POST["card_number"]).replace(" ", "").strip()
month = int(request.POST["card_expiry"].split("/")[0].strip())
year = int(request.POST["card_expiry"].split("/")[1].strip())
cvv = request.POST["card_cvv"]
user_data = {
"email": email,
"first_name": first_name,
"last_name": last_name,
"phone": phone,
}
total = int(round(order.total())) * 100
payload = json.dumps(
{
"amount": total,
"currency": "SAR",
"description": f"payment issued for {'email'}",
"callback_url": callback_url,
"source": {
"type": "creditcard",
"name": card_name,
"number": card_number,
"month": month,
"year": year,
"cvc": cvv,
"statement_descriptor": "Century Store",
"3ds": True,
"manual": False,
"save_card": False,
},
"metadata": user_data,
}
)
headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "")
response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
#
order.status = AbstractOrder.STATUS.NEW
order.save()
#
data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
models.PaymentHistory.objects.create(
user=request.user,
user_data=user_data,
amount=amount,
currency=data["currency"],
status=data["status"],
transaction_id=data["id"],
payment_date=data["created_at"],
gateway_response=data,
)
transaction_url = data["source"]["transaction_url"]
return transaction_url
# def get_user_quota(user):
# return user.dealer.quota

View File

@ -7,6 +7,7 @@ import numpy as np
# from rich import print
from random import randint
from decimal import Decimal
from datetime import timedelta
from calendar import month_name
from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse
@ -44,6 +45,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render, get_object_or_404, redirect
from plans.models import Order,PlanPricing,AbstractOrder,UserPlan,BillingInfo
from django.views.generic import (
View,
ListView,
@ -155,9 +157,11 @@ from .services import (
)
from .utils import (
CarFinanceCalculator,
create_user_dealer,
get_car_finance_data,
get_financial_values,
get_item_transactions,
handle_payment,
reserve_car,
# send_email,
get_user_type,
@ -285,32 +289,11 @@ def dealer_signup(request, *args, **kwargs):
if password != password_confirm:
return JsonResponse({"error": _("Passwords do not match")}, status=400)
try:
with transaction.atomic():
user = User.objects.create(username=email, email=email)
user.set_password(password)
user.save()
group = Group.objects.create(name=f"{user.pk}-Admin")
user.groups.add(group)
for perm in Permission.objects.filter(
content_type__app_label__in=["inventory", "django_ledger"]
):
group.permissions.add(perm)
StaffMember.objects.create(user=user)
models.Dealer.objects.create(
user=user,
name=name,
arabic_name=arabic_name,
crn=crn,
vrn=vrn,
phone_number=phone,
address=address,
)
return JsonResponse(
{"message": _("User created successfully")}, status=200
)
create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address)
return JsonResponse(
{"message": _("User created successfully")}, status=200
)
except Exception as e:
return JsonResponse({"error": str(e)}, status=400)
return render(
@ -1816,17 +1799,21 @@ class DealerDetailView(LoginRequiredMixin, DetailView):
car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer)
staff_count = dealer.staff_count
cars_count = models.Car.objects.filter(dealer=dealer).count()
quota_dict = get_user_quota(dealer.user)
allowed_users = quota_dict.get("Users", None)
allowed_cars = quota_dict.get("Cars", None)
# quota_dict = {}
# try:
# quota_dict = get_user_quota(dealer.user)
# except Exception as e:
# print(e)
# allowed_users = quota_dict.get("Users", None)
# allowed_cars = quota_dict.get("Cars", None)
user_quota = dealer.user_quota
context["car_makes"] = car_makes
context["staff_count"] = staff_count
context["cars_count"] = cars_count
context["allowed_users"] = allowed_users
context["allowed_cars"] = allowed_cars
context["allowed_users"] = dealer.user_quota
# context["allowed_cars"] = allowed_cars
context["quota_display"] = (
f"{staff_count}/{allowed_users}" if allowed_users is not None else "N/A"
f"{staff_count}/{user_quota}" if user_quota else "0"
)
return context
@ -2667,15 +2654,14 @@ class UserCreateView(
def form_valid(self, form):
dealer = get_user_type(self.request)
quota_dict = get_user_quota(dealer.user)
allowed_users = quota_dict.get("Users")
# quota_dict = get_user_quota(dealer.user)
# allowed_users = quota_dict.get("Users")
if allowed_users is None:
messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
return self.form_invalid(form)
# if allowed_users is None:
# messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
# return self.form_invalid(form)
current_staff_count = dealer.staff.count()
if current_staff_count >= allowed_users:
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)
@ -7655,3 +7641,62 @@ def ledger_unpost_all_journals(request, entity_slug, pk):
ledger.unpost()
ledger.save()
return redirect("journalentry_list", pk=ledger.pk)
def pricing_page(request):
plan_list = PlanPricing.objects.all()
form = forms.PaymentPlanForm()
return render(request, "pricing_page.html", {"plan_list": plan_list, "CURRENCY": "$","form":form})
# @require_POST
def submit_plan(request):
selected_plan_id = request.POST.get("selected_plan")
pp = PlanPricing.objects.get(pk=selected_plan_id)
order = Order.objects.create(
user=request.user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SAR",
tax=15,
status=AbstractOrder.STATUS.NEW
)
transaction_url = handle_payment(request,order)
return redirect(transaction_url)
def payment_callback(request):
payment_id = request.GET.get("id")
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
payment_status = request.GET.get("status")
order = Order.objects.filter(user=request.user,status=AbstractOrder.STATUS.NEW).first()
if payment_status == "paid":
billing_info,created = BillingInfo.objects.get_or_create(
user=request.user,
tax_number='123456789',
name='',
street='',
zipcode='12345',
city='Riyadh',
country='KSA',
)
if created:
userplan =UserPlan.objects.create(
user=request.user,
plan=order.plan,
active=True,
)
userplan.initialize()
order.complete_order()
history.status = "paid"
history.save()
invoice = order.get_invoices().first()
return render(request, "payment_success.html",{"order":order,"invoice":invoice})
elif payment_status == "failed":
history.status = "failed"
history.save()
message = request.GET.get('message')
return render(request, "payment_failed.html", {"message": message})

41
scripts/r.py Normal file
View File

@ -0,0 +1,41 @@
import json
import requests
from django.urls import reverse
from django.conf import settings
from django.contrib.auth.models import User
from inventory.models import PaymentHistory
from plans.models import Order, PlanPricing,AbstractOrder
def run():
request = {
"csrfmiddlewaretoken": [
"mAnzSt7JjHkHGb27cyF1AiFvuVF7iKhONDVUzyzYuH1U0b7hxXL89D1UA4XQInuu"
],
"selected_plan": ["33"],
"first_name": ["ismail"],
"last_name": ["mosa"],
"email": ["ismail.mosa.ibrahim@gmail.com"],
"phone": ["0566703794"],
"company": ["Tenhal"],
"card_name": ["ppppppppppp"],
"card_number": ["4111 1111 1111 1111"],
"card_expiry": ["08/28"],
"card_cvv": ["123"],
}
selected_plan_id = request.get("selected_plan")[0]
pp = PlanPricing.objects.get(pk=selected_plan_id)
user = User.objects.first()
order = Order.objects.create(
user=user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SAR",
tax=15,
status=AbstractOrder.STATUS.NEW
)
handle_payment(request,order)

View File

@ -44,7 +44,7 @@
<h6 class="mb-2 text-body-secondary">{% trans 'last login'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-0">{{ dealer.user.last_login|date:"D M d, Y H:i" }}</h4>
</div>
<div class="text-center me-1">
<div class="text-center me-1">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-0">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
</div>
@ -100,13 +100,22 @@
<div class="mb-5 mb-md-0 mb-lg-5">
<div class="d-sm-flex d-md-block d-lg-flex align-items-center mb-3">
<h3 class="mb-0 me-2">{{ dealer.user.userplan.plan|capfirst }}</h3>
{% if dealer.user.userplan.active %}
<span class="badge badge-phoenix fs-9 badge-phoenix-success"> <span class="badge-label">{% trans 'Active' %}</span><span class="ms-1" data-feather="check" style="height:16px;width:16px;"></span> </span>
{% if dealer.user.userplan %}
{% if not dealer.user.userplan.is_expired %}
<span class="badge badge-phoenix fs-9 badge-phoenix-success"> <span class="badge-label">{% trans 'Active' %}</span><span class="ms-1" data-feather="check" style="height:16px;width:16px;"></span> </span>
{% else %}
<span class="badge badge-phoenix fs-9 badge-phoenix-danger"> <span class="badge-label">{% trans 'Expired' %}</span><span class="ms-1" data-feather="times" style="height:16px;width:16px;"></span> </span>
<a href="{% url 'pricing_page' %}" class="btn btn-phoenix-secondary ms-2"><span class="fas fa-arrow-right me-2"></span>{{ _("Renew") }}</a>
{% endif %}
{% if dealer.user.userplan.plan.name != "Enterprise" %}
<a href="{% url 'pricing_page' %}" class="btn btn-phoenix-secondary ms-2"><span class="fas fa-arrow-right me-2"></span>{{ _("Upgrade") }}</a>
{% endif %}
{% else %}
<span class="badge badge-phoenix fs-9 badge-phoenix-danger"> <span class="badge-label">{% trans 'Expired' %}</span><span class="ms-1" data-feather="times" style="height:16px;width:16px;"></span> </span>
<span class="text-body-tertiary fw-semibold">You have no active plan.</span> <a href="{% url 'pricing_page' %}" class="btn btn-phoenix-secondary ms-2"><span class="fas fa-arrow-right me-2"></span>{{ _("Subscribe") }}</a>
{% endif %}
</div>
<p class="fs-9 text-body-tertiary">{% trans 'Active until' %}: {{ dealer.user.userplan.expire}}</p>
<p class="fs-9 text-body-tertiary">{% trans 'Active until' %}: {{ dealer.user.userplan.expire}}&nbsp;&nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left}}</small></p>
<div class="d-flex align-items-end mb-md-5 mb-lg-0">
<h4 class="fw-bolder me-1">{{ dealer.user.userplan.plan.planpricing_set.first.price }}<span class="currency"> {{ CURRENCY }}</span></h4>
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month")}}</h5>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<main class="main">
<!-- Page Title -->
<div class="page-title light-background">
<div class="container d-lg-flex justify-content-between align-items-center">
<h1 class="mb-2 mb-lg-0">{% trans "Payment Failed" %}</h1>
<nav class="breadcrumbs">
<ol>
<li><a href="">{% trans "Home"%}</a></li>
<li class="current">{% trans "Failed"%}</li>
</ol>
</nav>
</div>
</div><!-- End Page Title -->
<!-- Failed Section -->
<section class="section">
<div class="container text-center" data-aos="fade-up">
<div class="py-5">
<i class="bi bi-x-circle-fill text-danger" style="font-size: 5rem;"></i>
<h2 class="mt-4">{% trans "Payment Failed"%}</h2>
{% if message %}
<p class="lead">{{message}}.</p>
{% else %}
<p class="lead">{% trans "We couldn't process your payment. Please try again"%}.</p>
{% endif %}
<a href="" class="btn btn-primary mt-3">
<i class="bi bi-house-door"></i> {% trans "Back to Home"%}
</a>
</div>
</div>
</section><!-- /Failed Section -->
</main>
{% endblock content %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<main class="main">
<!-- Page Title -->
<div class="page-title light-background">
<div class="container d-lg-flex justify-content-between align-items-center">
<h1 class="mb-2 mb-lg-0">{% trans "Payment Successful"%}</h1>
<nav class="breadcrumbs">
<ol>
<li><a href="#">Home</a></li>
<li class="current">Success</li>
</ol>
</nav>
</div>
</div><!-- End Page Title -->
<!-- Success Section -->
<section class="section">
<div class="container text-center" data-aos="fade-up">
<div class="py-5">
<i class="bi bi-check-circle-fill text-success" style="font-size: 5rem;"></i>
<h2 class="mt-4">Thank You!</h2>
<p class="lead">Your payment was successful. Your order is being processed.</p>
{% if invoice %}
<a href="{% url 'invoice_preview_html' invoice.pk %}" class="btn btn-primary mt-3">
<i class="bi bi-house-door"></i> View Invoice
</a>
{% endif %}
<a href="" class="btn btn-primary mt-3">
<i class="bi bi-house-door"></i> Back to Home
</a>
</div>
</div>
</section><!-- /Success Section -->
</main>
{% endblock content %}

View File

@ -1,36 +1,52 @@
{% extends 'base.html' %}
{% load i18n custom_filters%}
{% load i18n custom_filters %}
{% block content %}
<div class="container-fluid px-3 px-md-5 py-4">
<!-- Account Details Section -->
<div class="row my-3">
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">{% trans "Your Account" %}</h5>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom-0 py-3">
<h5 class="mb-0 fw-semibold">{% trans "Your Account" %}</h5>
</div>
<div class="card-body">
<div class="row mb-0">
<div class="col-sm-2">{% trans "Account" %}:</div>
<div class="col-sm-10">{{ user.dealer.get_local_name }}</div>
<div class="col-sm-2">{% trans "Status" %}:</div>
<div class="col-sm-10">
{% if userplan.active %}
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Active" %}</span>
{% else %}
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Expired" %}</span>
{% endif %}
<div class="card-body pt-0">
<div class="row g-3">
<div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Account" %}</span>
<span class="fw-semibold">{{ user.dealer.get_local_name }}</span>
</div>
</div>
<div class="col-sm-2">{% trans "Active until" %}:</div>
<div class="col-sm-10">{{ userplan.expire }}</div>
<div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Status" %}</span>
{% if userplan.active %}
<span class="badge bg-success bg-opacity-10 text-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-danger bg-opacity-10 text-danger">{% trans "Expired" %}</span>
{% endif %}
</div>
</div>
<div class="col-sm-2">{% trans "Plan" %}:</div>
<div class="col-sm-10">
{{ userplan.plan }}
<a href="{% url 'upgrade_plan' %}" class="btn btn-sm btn-phoenix-primary ml-2">{% trans "Upgrade" %}</a>
<div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Active until" %}</span>
<span class="fw-semibold">{{ userplan.expire }}</span>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
<span class="text-muted small">{% trans "Plan" %}</span>
<div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold">{{ userplan.plan }}</span>
<a href="{% url 'pricing_page' %}" class="btn btn-sm btn-outline-primary">
{% trans "Upgrade" %}
</a>
</div>
</div>
</div>
</div>
</div>
@ -41,21 +57,15 @@
<!-- Plan Details Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">{% trans "Plan Details" %}</h5>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom-0 py-3">
<h5 class="mb-0 fw-semibold">{% trans "Plan Details" %}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
{% include "plans/plan_table.html" %}
</div>
</div>
{% include "plans/plan_table.html" %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,171 +1,204 @@
<div class="container my-5">
<!-- Header Section (unchanged) -->
<div class="d-flex justify-content-between align-items-start mb-5">
{% if logo_url %}
<img src="{{ logo_url }}" alt="company logo">
<img src="{{ logo_url }}" alt="Company Logo" class="img-fluid" style="max-height: 80px; max-width: 200px;">
{% endif %}
<div style="float:right; text-align: right;">
<h1>
<label><span class="en">{% if invoice.type == invoice.INVOICE_TYPES.INVOICE %}Invoice ID{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}Order confirmation ID{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.DUPLICATE %}Invoice (duplicate) ID{% endif %}</span></label> <span id="full_number">{{ invoice.full_number }}</span>
<div class="text-end">
<div class="d-inline-block p-3 bg-light rounded-3">
<h1 class="h4 mb-1">
<span class="text-muted small">
{% if invoice.type == invoice.INVOICE_TYPES.INVOICE %}Invoice{% endif %}
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}Order Confirmation{% endif %}
{% if invoice.type == invoice.INVOICE_TYPES.DUPLICATE %}Invoice (Duplicate){% endif %}
</span>
<div id="full_number" class="fw-bold fs-3">{{ invoice.full_number }}</div>
</h1>
<h2>{% if not copy %}ORIGINAL{% else %}COPY{% endif %}</h2>
<p> <label> <span class="en">Issued</span></label> {{ invoice.issued|date:"Y-m-d" }}</p>
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<p> <label> <span class="en">Date of order</span></label> {{ invoice.selling_date|date:"Y-m-d" }}</p>
{% else %}
<p>&nbsp;</p>
{% endif %}
<div class="badge bg-{% if copy %}warning{% else %}primary{% endif %} bg-opacity-10 text-{% if copy %}warning{% else %}primary{% endif %} mb-2">
{{ copy|yesno:"COPY,ORIGINAL" }}
</div>
<div class="d-flex flex-column text-start">
<div class="mb-1"><span class="text-muted">Issued:</span> <strong>{{ invoice.issued|date:"F j, Y" }}</strong></div>
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<div><span class="text-muted">Order Date:</span> <strong>{{ invoice.selling_date|date:"F j, Y" }}</strong></div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Company Information (unchanged) -->
<div class="row mb-5 g-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Seller</h5>
</div>
<div class="card-body">
<address class="mb-0">
<strong>{{ invoice.issuer_name }}</strong><br>
{{ invoice.issuer_street }}<br>
{{ invoice.issuer_zipcode }} {{ invoice.issuer_city }}<br>
{{ invoice.issuer_country.name }}<br>
<span class="text-muted">VAT ID:</span> {{ invoice.issuer_tax_number }}
</address>
</div>
</div>
</div>
<table style="width: 100%; margin-bottom: 40px; font-size: 12px;" >
<tr>
<td style="width: 50%;">
</td>
<td style="width: 50%; padding-right: 4em; font-weight: bold; font-size: 15px;" id="shipping">
<strong> <label><span class="en">Shipping address</span></label></strong><br><br>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Buyer</h5>
</div>
<div class="card-body">
<address class="mb-0">
<strong>{{ invoice.buyer_name }}</strong><br>
{{ invoice.buyer_street }}<br>
{{ invoice.buyer_zipcode }} {{ invoice.buyer_city }}<br>
{{ invoice.buyer_country.name }}<br>
{% if invoice.buyer_tax_number %}
<span class="text-muted">VAT ID:</span> {{ invoice.buyer_tax_number }}
{% endif %}
</address>
</div>
</div>
</div>
</div>
{{ invoice.shipping_name }}<br>
{{ invoice.shipping_street }}<br>
{{ invoice.shipping_zipcode }} {{ invoice.shipping_city }}<br>
{{ invoice.buyer_country.code }} - {{ invoice.buyer_country.name }}
<!-- Shipping Address (unchanged) -->
{% if invoice.shipping_name %}
<div class="card border-0 shadow-sm mb-5">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Shipping Address</h5>
</div>
<div class="card-body">
<address class="mb-0">
<strong>{{ invoice.shipping_name }}</strong><br>
{{ invoice.shipping_street }}<br>
{{ invoice.shipping_zipcode }} {{ invoice.shipping_city }}<br>
{{ invoice.buyer_country.name }}
</address>
</div>
</div>
{% endif %}
</td>
</tr>
<tr>
<td style="width: 50%; vertical-align: top;">
<strong> <label><span class="en">Seller</span></label></strong><br><br>
{{ invoice.issuer_name }}<br>
{{ invoice.issuer_street }}<br>
{{ invoice.issuer_zipcode }} {{ invoice.issuer_city}}<br>
{{ invoice.issuer_country.code }} - {{ invoice.issuer_country.name }}<p>
<label><span class="en">VAT ID</span></label> {{ invoice.issuer_tax_number }}<br>
</td>
<td style="width: 50%; vertical-align: top;">
<strong> <label> <span class="en">Buyer</span></label></strong><br><br>
{{ invoice.buyer_name }}<br>
{{ invoice.buyer_street }}<br>
{{ invoice.buyer_zipcode }} {{ invoice.buyer_city }}<br>
{{ invoice.buyer_country.code }} - {{ invoice.buyer_country.name }}
{% if invoice.buyer_tax_number %}
<p>
<label><span class="en">VAT ID</span></label> {{ invoice.buyer_tax_number }}
</p>
<!-- Items Table - Now with horizontal scrolling -->
<div class="card border-0 shadow-sm mb-5">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Invoice Items</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="overflow-x: auto;">
<div style="min-width: 800px;"> <!-- Minimum width to ensure scrolling on smaller screens -->
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="text-center sticky-col" style="width: 5%; left: 0; background-color: #f8f9fa; z-index: 1;">#</th>
<th style="width: 25%; min-width: 200px;">Description</th>
<th class="text-end" style="width: 10%; min-width: 100px;">Unit Price</th>
<th class="text-center" style="width: 8%; min-width: 80px;">Qty.</th>
<th class="text-center" style="width: 8%; min-width: 80px;">Unit</th>
{% if invoice.rebate %}
<th class="text-center" style="width: 8%; min-width: 80px;">Rebate</th>
{% endif %}
<br>
</td>
</tr>
</table>
<table style="margin-bottom: 40px; width: 100%;" id="items">
<thead>
<tr>
<td>
</td>
<td>
<span class="en">Description</span>
</td>
<td>
<span class="en">Unit&nbsp;price</span>
</td>
<td>
<span class="en">Qty.</span>
</td>
<td>
</td>
{% if invoice.rebate %}
<td>
<span class="en">Rebate</span>
</td>
{% endif %}
<td>
<span class="en">Subtotal</span>
</td>
<td style="width: 3%;">
<span class="en">VAT</span>
</td>
<td>
<span class="en">VAT&nbsp;Amount</span>
</td>
<td style="width: 8%;">
<span class="en">Subtotal&nbsp;with&nbsp;TAX/VAT</span>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
1
</td>
<td class="center">{{ invoice.item_description }}</td>
<td class="number">{{ invoice.unit_price_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="center">{{ invoice.quantity }}</td>
<td class="center"><span class="en">units</span></td>
{% if invoice.rebate %}
<td class="number">{{ invoice.rebate|floatformat:2 }}&nbsp;%</td>
{% endif %}
<td class="number">{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="number">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="en">n/a</span>{% endif %}</td>
<td class="number">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
<td class="number">{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="{% if invoice.rebate %}6{% else %}5{% endif %}" style="background-color: #EEE;"><label><span class="en">Total</span></label> </td>
<td>{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td>{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="en">n/a</span>{% endif %}</td>
<td>{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
<td>{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tfoot>
</table>
<div style="width: 100%;">
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<strong><label><span class="en">Payment</span></label></strong> <label> <span class="en">electronic payment</span></label><br><br>
{% endif %}
<strong><label><span class="en">Payment till</span></label></strong>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}
{% else %}
<label> <span class="en"> paid</span></label>
{% endif %}
{{ invoice.payment_date|date:"Y-m-d" }}
<br><br>
<hr>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}<p><span class="en">This document <strong>is not</strong> an invoice.</span></p> {% endif %}
{% if invoice.tax == None and invoice.is_UE_customer %}
<p>
<span class="en">-Reverse charge.</span>
</p>
{% endif %}
<th class="text-end" style="width: 10%; min-width: 100px;">Subtotal</th>
<th class="text-center" style="width: 8%; min-width: 80px;">VAT</th>
<th class="text-end" style="width: 10%; min-width: 100px;">VAT Amount</th>
<th class="text-end" style="width: 10%; min-width: 100px;">Total</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center sticky-col" style="left: 0; background-color: white; z-index: 1;">1</td>
<td>{{ invoice.item_description }}</td>
<td class="text-end">{{ invoice.unit_price_net|floatformat:2 }} {{ invoice.currency }}</td>
<td class="text-center">{{ invoice.quantity }}</td>
<td class="text-center">units</td>
{% if invoice.rebate %}
<td class="text-center">{{ invoice.rebate|floatformat:2 }}%</td>
{% endif %}
<td class="text-end">{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}</td>
<td class="text-center">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}%{% else %}n/a{% endif %}</td>
<td class="text-end">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}</td>
<td class="text-end fw-bold">{{ invoice.total|floatformat:2 }} {{ invoice.currency }}</td>
</tr>
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="{% if invoice.rebate %}6{% else %}5{% endif %}" class="text-end fw-bold sticky-col" style="left: 0; background-color: #f8f9fa; z-index: 1;">Total</td>
<td class="text-end fw-bold">{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}</td>
<td class="text-center fw-bold">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}%{% else %}n/a{% endif %}</td>
<td class="text-end fw-bold">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}</td>
<td class="text-end fw-bold text-primary">{{ invoice.total|floatformat:2 }} {{ invoice.currency }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<!-- Payment Information (unchanged) -->
<div class="row">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Payment Information</h5>
</div>
<div class="card-body">
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<div class="mb-2">
<span class="text-muted">Method:</span>
<strong>Electronic Payment</strong>
</div>
{% endif %}
<div class="mb-2">
<span class="text-muted">Due Date:</span>
<strong>{{ invoice.payment_date|date:"F j, Y" }}</strong>
</div>
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<div class="alert alert-success p-2 mb-0">
<i class="fas fa-check-circle me-2"></i> Payment Received
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Notes</h5>
</div>
<div class="card-body">
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}
<div class="alert alert-warning p-2 mb-2">
<i class="fas fa-exclamation-triangle me-2"></i> This document is not an invoice.
</div>
{% endif %}
{% if invoice.tax == None and invoice.is_UE_customer %}
<div class="alert alert-info p-2 mb-0">
<i class="fas fa-info-circle me-2"></i> Reverse charge applied.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Footer (unchanged) -->
<div class="mt-5 pt-4 border-top text-center text-muted small">
<p class="mb-1">Thank you for your business!</p>
<p class="mb-0">If you have any questions about this invoice, please contact us.</p>
</div>
</div>
<style>
.sticky-col {
position: sticky;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.table-responsive {
-webkit-overflow-scrolling: touch;
}
</style>

View File

@ -1,89 +1,121 @@
{% load i18n%}
<div class="table-responsive mt-4">
<table class="plan_table table border-top border-translucent fs-9 mb-4">
<thead class="">
<tr>
<th scope="col"></th>
{% load i18n %}
<div class="table-responsive">
<table class="table table-hover table-compare mb-0">
<thead>
<tr class="border-bottom border-200">
<th style="width: 30%; min-width: 280px;" class="ps-4 bg-white position-sticky start-0 border-end border-200"></th>
{% for plan in plan_list %}
<th scope="col" class="plan_header text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
{% if plan.url %}<a href="{{ plan.url }}" class="info_link plan text-decoration-none">{% endif %}
<span class="plan_name font-weight-bold">{{ plan.name }}</span>
{% if plan == userplan.plan %}
<span class="current current_plan badge badge-phoenix badge-phoenix-success">{% trans "Current Plan" %}</span>
{% endif %}
<th class="text-center py-3 {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}" style="width: calc(70%/{{ plan_list|length }});">
<div class="d-flex flex-column align-items-center">
{% if plan.url %}<a href="{{ plan.url }}" class="text-decoration-none text-dark">{% endif %}
<h6 class="mb-1 fw-bold">{{ plan.name }}</h6>
{% if plan == userplan.plan %}
<span class="badge bg-success bg-opacity-10 text-success mt-1">{% trans "Current Plan" %}</span>
{% endif %}
{% if plan.url %}</a>{% endif %}
</th>
</div>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for quota_row in plan_table %}
<tr class="quota_row">
<th scope="row" class="quota_header align-middle">
{% if quota_row.0.url %}<a href="{{ quota_row.0.url }}" class="info_link quota text-decoration-none">{% endif %}
<span class="quota_name fw-bold">{{ quota_row.0.name }}</span>
<small class="quota_description d-block text-muted">{{ quota_row.0.description }}</small>
<tr class="border-bottom border-200 {% cycle 'bg-white' 'bg-light' %}">
<th class="ps-4 fw-normal position-sticky start-0 bg-inherit border-end border-200">
<div class="d-flex flex-column py-2">
{% if quota_row.0.url %}<a href="{{ quota_row.0.url }}" class="text-decoration-none text-dark">{% endif %}
<span class="fw-semibold">{{ quota_row.0.name }}</span>
<small class="text-muted">{{ quota_row.0.description }}</small>
{% if quota_row.0.url %}</a>{% endif %}
</th>
{% for plan_quota in quota_row.1 %}
<td class="align-middle text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
{% if plan_quota != None %}
{% if quota_row.0.is_boolean %}
{% if plan_quota.value %}<i class="fas fa-check text-success"></i>{% else %}<i class="fas fa-times text-danger"></i>{% endif %}
{% else %}
{% if plan_quota.value == None %}{% trans 'No Limit' %}{% else %}{{ plan_quota.value }} {{ quota_row.0.unit }}{% endif %}
{% endif %}
</div>
</th>
{% for plan_quota in quota_row.1 %}
<td class="text-center py-2 align-middle {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}">
{% if plan_quota != None %}
{% if quota_row.0.is_boolean %}
{% if plan_quota.value %}
<i class="fas fa-check-circle text-success fs-6"></i>
{% else %}
<i class="fas fa-times-circle text-danger fs-6"></i>
{% endif %}
</td>
{% endfor %}
</tr>
{% else %}
<span class="d-block">
{% if plan_quota.value == None %}
<span class="badge bg-info bg-opacity-10 text-info">{% trans 'No Limit' %}</span>
{% else %}
<span class="fw-semibold">{{ plan_quota.value }}</span>
{% if quota_row.0.unit %}
<span class="text-muted small">{{ quota_row.0.unit }}</span>
{% endif %}
{% endif %}
</span>
{% endif %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
<tfoot class="">
<tr>
<th scope="col"></th>
<th colspan="{{ plan_list|length }}" class="text-center font-weight-bold py-3">{% trans 'Pricing' %}</th>
<tfoot>
<tr class="border-bottom border-200">
<th class="ps-4 bg-white position-sticky start-0 border-end border-200"></th>
<th colspan="{{ plan_list|length }}" class="text-center py-3 bg-light">
<h6 class="mb-0 fw-bold">{% trans 'Pricing' %}</h6>
</th>
</tr>
{% if user.is_authenticated %}
<tr>
<th scope="col"></th>
{% for plan in plan_list %}
<th class="planpricing_footer text-center fw-bold {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
{% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %}
<a href="{% url 'create_order_plan_change' pk=plan.id %}" class="change_plan btn btn-sm btn-outline-primary">{% trans "Change" %}</a>
{% endif %}
</th>
{% endfor %}
</tr>
{% endif %}
<tr>
<th scope="col"></th>
<tr class="border-bottom border-200">
<th class="ps-4 bg-white position-sticky start-0 border-end border-200"></th>
{% for plan in plan_list %}
<th class="planpricing_footer text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
{% if plan.available %}
<ul class="list-unstyled">
{% if not plan.is_free %}
{% for plan_pricing in plan.planpricing_set.all %}
{% if plan_pricing.visible %}
<li class="mb-2">
{% if plan_pricing.pricing.url %}<a href="{{ plan_pricing.pricing.url }}" class="info_link pricing text-decoration-none">{% endif %}
<span class="plan_pricing_name font-weight-bold">{{ plan_pricing.pricing.name }}</span>
<small class="plan_pricing_period d-block text-muted">({{ plan_pricing.pricing.period }} {% trans "day" %})</small>
{% if plan_pricing.pricing.url %}</a>{% endif %}
<span class="plan_pricing_price d-block font-weight-bold">{{ plan_pricing.price }}&nbsp;{{ CURRENCY }}</span>
<td class="text-center py-3 {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}">
{% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %}
<a href="{% url 'create_order_plan_change' pk=plan.id %}" class="btn btn-sm btn-outline-primary px-3">
{% trans "Change" %}
</a>
{% endif %}
</td>
{% endfor %}
</tr>
{% endif %}
<tr>
<th class="ps-4 bg-white position-sticky start-0 border-end border-200"></th>
{% for plan in plan_list %}
<td class="p-3 {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}">
{% if plan.available %}
<div class="d-flex flex-column gap-2">
{% if not plan.is_free %}
{% for plan_pricing in plan.planpricing_set.all %}
{% if plan_pricing.visible %}
<div class="p-3 rounded-2 {% if plan_pricing.plan == userplan.plan %}bg-success-soft{% else %}bg-light{% endif %}">
{% if plan_pricing.pricing.url %}<a href="{{ plan_pricing.pricing.url }}" class="text-decoration-none text-dark">{% endif %}
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-semibold">{{ plan_pricing.pricing.name }}</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary fs-10">{{ plan_pricing.pricing.period }} {% trans "days" %}</span>
</div>
{% if plan_pricing.pricing.url %}</a>{% endif %}
<div class="d-flex justify-content-between align-items-end mt-2">
<span class="fs-5 fw-bold">{{ plan_pricing.price }} <span class="currency">{{ CURRENCY }}</span></span>
{% if plan_pricing.plan == userplan.plan or userplan.is_expired or userplan.plan.is_free %}
<a href="{% url 'create_order_plan' pk=plan_pricing.pk %}" class="buy btn btn-sm btn-success">{% trans "Buy" %}</a>
<a href="{% url 'pricing_page' %}" class="btn btn-sm btn-success">
{% trans "Buy" %}
</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
{% else %}
<li class="mb-2">
<span class="plan_pricing_name font-weight-bold">{% trans "Free" %}</span>
<small class="plan_pricing_period d-block text-muted">({% trans "no expiry" %})</small>
<span class="plan_pricing_price d-block font-weight-bold">0&nbsp;{{ CURRENCY }}</span>
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="p-3 rounded-2 bg-light">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-semibold">{% trans "Free" %}</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary fs-10">{% trans "no expiry" %}</span>
</div>
<div class="d-flex justify-content-between align-items-end mt-2">
<span class="fs-5 fw-bold">0 {{ CURRENCY }}</span>
{% if plan != userplan.plan or userplan.is_expired %}
<a href="{% url 'create_order_plan_change' pk=plan.id %}" class="change_plan btn btn-sm btn-phoenix-primary">
<a href="{% url 'create_order_plan_change' pk=plan.id %}" class="btn btn-sm btn-outline-primary">
{% if userplan.is_expired %}
{% trans "Select" %}
{% else %}
@ -91,21 +123,22 @@
{% endif %}
</a>
{% endif %}
</li>
{% endif %}
</ul>
{% else %}
<span class="plan_not_available text-muted">
{% url 'upgrade_plan' as upgrade_url %}
{% blocktrans %}
This plan is not available anymore and cannot be extended.<br>
You need to upgrade your account to any of <a href="{{ upgrade_url }}" class="text-primary">currently available plans</a>.
{% endblocktrans %}
</span>
{% endif %}
</th>
</div>
</div>
{% endif %}
</div>
{% else %}
<div class="alert alert-warning bg-warning-soft border-0 small mb-0">
{% url 'upgrade_plan' as upgrade_url %}
{% blocktrans %}
<p class="mb-1">This plan is no longer available.</p>
<a href="{{ upgrade_url }}" class="fw-bold text-warning">Upgrade to current plans →</a>
{% endblocktrans %}
</div>
{% endif %}
</td>
{% endfor %}
</tr>
</tfoot>
</table>
</div>
</div>

352
templates/pricing_page.html Normal file
View File

@ -0,0 +1,352 @@
{% extends 'base.html' %}
{% load i18n static %}
{% load custom_filters %}
{% block customCSS %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-X..." crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.pricing-card .card {
transition: all 0.3s ease-in-out;
}
.pricing-card .card.selected {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.4);
background-color: #f0f8ff;
}
.btn-check:checked + .btn .card {
/* fallback if JS fails */
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.card.selected {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.4);
background-color: #f0f8ff;
}
.summary-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
}
.summary-box h5 {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
.summary-item {
margin-bottom: 0.75rem;
}
.form-label {
font-weight: 500;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="container py-5">
<h1 class="text-center mb-5">Choose Your Plan</h1>
<form method="POST" action="{% url 'submit_plan' %}" id="wizardForm">
{% csrf_token %}
<!-- Step 1: Plan Selection -->
<div class="step" id="step1">
<h4 class="mb-4">1. Select a Plan</h4>
<div class="row g-4">
{% for pp in plan_list %}
<div class="col-md-6 col-lg-3">
<input type="radio" class="btn-check" name="selected_plan" id="plan_{{ forloop.counter }}" value="{{ pp.id }}"
data-name="{{ pp.plan.name }}" data-price="{{ pp.price }}" autocomplete="off" {% if forloop.first %}checked{% endif %}>
<label class="btn w-100 p-0 pricing-card" for="plan_{{ forloop.counter }}">
<div class="card h-100 border border-2 rounded-4">
<div class="card-body p-4">
<h4 class="mb-3">{{ pp.plan.name }}</h4>
<h5 class="mb-4">{{ pp.price }} <span class="currency">{{ CURRENCY }}</span><span class="fs-6 fw-normal">/ {{ _("Per month") }}</span></h5>
<h6>{{ _("Included") }}</h6>
<ul class="fa-ul ps-3">
{% if pp.plan.description %}
{% for line in pp.plan.description|splitlines %}
<li class="mb-2">
<span class="fa-li"><i class="fas fa-check text-primary"></i></span>
{{ line }}
</li>
{% endfor %}
{% endif %}
</ul>
</div>
</div>
</label>
</div>
{% endfor %}
</div>
</div>
<!-- Step 2: User Info -->
<div class="step d-none" id="step2">
<h4 class="mb-4">2. Enter Your Information</h4>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">First Name</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" name="first_name" id="first_name" class="form-control" required placeholder="John" value="{{ request.user.first_name }}">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Last Name</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" name="last_name" id="last_name" class="form-control" required placeholder="Doe" value="{{ request.user.last_name }}">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Email Address</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input type="email" name="email" id="email" class="form-control" required placeholder="email@example.com" value="{{ request.user.email }}">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Phone Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-phone"></i></span>
<input type="text" name="phone" id="phone" class="form-control" required placeholder="056XXXXXXX" value="{{ request.user.dealer.phone_number.raw_input }}">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Company</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-building"></i></span>
<input type="text" name="company" id="company" class="form-control" placeholder="ABC Company">
</div>
</div>
</div>
</div>
<!-- Step 3: Payment -->
<div class="step d-none" id="step3">
<h4 class="mb-4">3. Payment Information</h4>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Cardholder Name</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" name="card_name" id="card_name" class="form-control" placeholder="John Doe" required>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Card Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-credit-card"></i></span>
<input type="text" name="card_number" id="card_number" class="form-control" placeholder="1234 5678 9012 3456"
maxlength="19" pattern="^\d{4}\s\d{4}\s\d{4}\s\d{4}$"
inputmode="numeric" required title="Enter a 16-digit card number">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Expiry Date (MM/YY)</label>
<div class="input-group">
<span class="input-group-text"><i class="far fa-calendar-alt"></i></span>
<input type="text" name="card_expiry" id="card_expiry" class="form-control" placeholder="08/28"
maxlength="5" pattern="^(0[1-9]|1[0-2])\/\d{2}$"
inputmode="numeric" required title="Enter expiry in MM/YY format">
</div>
</div>
<div class="col-md-2">
<label class="form-label">CVV</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="text" name="card_cvv" id="card_cvv" class="form-control" placeholder="123"
maxlength="3" pattern="^\d{3}$"
inputmode="numeric" required title="Enter 3-digit CVV">
</div>
</div>
</div>
</div>
<!-- Step 4: Confirmation -->
<div class="step d-none" id="step4">
<h4 class="mb-4">4. Confirm Your Information</h4>
<div class="summary-box">
<h5><i class="fas fa-file-invoice-dollar me-2"></i>Order Summary</h5>
<div class="summary-item"><i class="fas fa-box"></i><strong>Plan:</strong> <span id="summary_plan"></span></div>
<div class="summary-item"><i class="fas fa-tag"></i><strong>Price:</strong> <span id="summary_price"></span></div>
<div class="summary-item"><i class="fas fa-receipt"></i><strong>Tax (15%):</strong> <span id="summary-tax">0.00</span> <span class="currency">{{ CURRENCY }}</span></div>
<hr>
<div class="summary-item"><i class="fas fa-hand-holding-usd"></i><strong>Total:</strong> <span id="summary-total">0.00</span> {{ CURRENCY }}</div>
<h5 class="mt-4"><i class="fas fa-user me-2"></i>User Information</h5>
<div class="summary-item"><i class="fas fa-signature"></i><strong>Name:</strong> <span id="summary_name"></span></div>
<div class="summary-item"><i class="fas fa-envelope"></i><strong>Email:</strong> <span id="summary_email"></span></div>
<div class="summary-item"><i class="fas fa-building"></i><strong>Company:</strong> <span id="summary_company"></span></div>
<div class="summary-item"><i class="fas fa-phone"></i><strong>Phone:</strong> <span id="summary_phone"></span></div>
<h5 class="mt-4"><i class="fas fa-credit-card me-2"></i>Payment</h5>
<div class="summary-item"><i class="fas fa-user"></i><strong>Cardholder:</strong> <span id="summary_card_name"></span></div>
<div class="summary-item"><i class="fas fa-credit-card"></i><strong>Card Number:</strong> <span id="summary_card_number"></span></div>
<div class="summary-item"><i class="far fa-calendar-alt"></i><strong>Expiry:</strong> <span id="summary_card_expiry"></span></div>
</div>
</div>
<!-- Navigation -->
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" id="prevBtn" disabled>Previous</button>
<button type="button" class="btn btn-primary" id="nextBtn">Next</button>
<button type="submit" class="btn btn-success d-none" id="submitBtn">Confirm</button>
</div>
</form>
</div>
{% endblock content %}
{% block customJS %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const radios = document.querySelectorAll('.btn-check');
radios.forEach(radio => {
radio.addEventListener('change', function () {
document.querySelectorAll('.pricing-card .card').forEach(card => {
card.classList.remove('selected');
});
if (this.checked) {
const selectedCard = document.querySelector(`label[for="${this.id}"] .card`);
selectedCard.classList.add('selected');
}
});
// Trigger change on page load if checked
if (radio.checked) {
const selectedCard = document.querySelector(`label[for="${radio.id}"] .card`);
selectedCard.classList.add('selected');
}
});
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
let currentStep = 0;
const steps = document.querySelectorAll(".step");
const nextBtn = document.getElementById("nextBtn");
const prevBtn = document.getElementById("prevBtn");
const submitBtn = document.getElementById("submitBtn");
function showStep(index) {
steps.forEach((step, i) => {
step.classList.toggle("d-none", i !== index);
});
prevBtn.disabled = index === 0;
nextBtn.classList.toggle("d-none", index === steps.length - 1);
submitBtn.classList.toggle("d-none", index !== steps.length - 1);
// If last step, populate summary
if (index === steps.length - 1) {
populateSummary();
}
}
function populateSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
/*const currencyElement = document.createElement("span");
currencyElement.classList.add("currency");
currencyElement.textContent = "{{ CURRENCY }}";
document.getElementById("summary_price").appendChild(currencyElement);*/
document.getElementById("summary_name").textContent = document.getElementById("first_name").value + " " + document.getElementById("last_name").value;
document.getElementById("summary_email").textContent = document.getElementById("email").value;
document.getElementById("summary_company").textContent = document.getElementById("company").value;
document.getElementById("summary_phone").textContent = document.getElementById("phone").value;
document.getElementById("summary_card_name").textContent = document.getElementById("card_name").value;
document.getElementById("summary_card_number").textContent = maskCard(document.getElementById("card_number").value);
document.getElementById("summary_card_expiry").textContent = document.getElementById("card_expiry").value;
}
function maskCard(cardNumber) {
const last4 = cardNumber.slice(-4);
return "**** **** **** " + last4;
}
nextBtn.addEventListener("click", () => {
if (currentStep < steps.length - 1) {
currentStep++;
showStep(currentStep);
}
});
prevBtn.addEventListener("click", () => {
if (currentStep > 0) {
currentStep--;
showStep(currentStep);
}
});
// Highlight selected plan
document.querySelectorAll(".btn-check").forEach(input => {
input.addEventListener("change", () => {
document.querySelectorAll(".pricing-card .card").forEach(card => card.classList.remove("selected"));
document.querySelector(`label[for="${input.id}"] .card`).classList.add("selected");
});
if (input.checked) {
document.querySelector(`label[for="${input.id}"] .card`).classList.add("selected");
}
});
showStep(currentStep);
//////////////////////////////////////////////////////////////////////////////////////
const cardNumberInput = document.getElementById("card_number");
cardNumberInput.addEventListener("input", function (e) {
let val = cardNumberInput.value.replace(/\D/g, "").substring(0, 16); // Only digits
let formatted = val.replace(/(.{4})/g, "$1 ").trim();
cardNumberInput.value = formatted;
});
// Format expiry date as MM/YY
const expiryInput = document.getElementById("card_expiry");
expiryInput.addEventListener("input", function (e) {
let val = expiryInput.value.replace(/\D/g, "").substring(0, 4); // Only digits
if (val.length >= 3) {
val = val.substring(0, 2) + "/" + val.substring(2);
}
expiryInput.value = val;
});
//////////////////////////////////////////////////////////////////////////////////////
const planInputs = document.querySelectorAll("input[name='selected_plan']");
const summaryPlanName = document.getElementById("summary_plan");
const summaryPrice = document.getElementById("summary_price"); //summary_price
const summaryTax = document.getElementById("summary-tax");
const summaryTotal = document.getElementById("summary-total");
function updateSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (selectedPlan) {
const price = parseFloat(selectedPlan.dataset.price);
const tax = price * 0.15;
const total = price + tax;
document.getElementById("summary_price").textContent = price.toFixed(2) + " {{ CURRENCY }}";
document.getElementById("summary-tax").textContent = tax.toFixed(2) + " {{ CURRENCY }}";
document.getElementById("summary-total").textContent = total.toFixed(2) + " {{ CURRENCY }}";
}
}
planInputs.forEach(input => input.addEventListener("change", updateSummary));
updateSummary(); // Initial call
});
</script>
{% endblock customJS %}

View File

@ -2,6 +2,14 @@
{% load custom_filters %}
{% load i18n static %}
{% block extraCSS %}
<style>
.btn-check:checked + .btn .card {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
</style>
{% endblock extraCSS %}
{% block content %}
@ -84,31 +92,36 @@
<div class="bg-holder d-none d-xl-block" style="background-image:url({% static 'images/bg/bg-left-29.png' %});background-size:auto;background-position:-15%;"></div>
<div class="container-medium position-relative">
<h2 class="mb-7">{{ _("Pricing") }}</h2>
<div class="row g-3 mb-7 mb-lg-11">
{% for plan in plan_list %}
<div class="col-lg-3">
<div class="pricing-card h-100">
<div class="card bg-transparent border border-1 border-primary-dark rounded-4">
<div class="card-body p-7">
<h2 class="mb-5">{{ plan.name }}</h2>
<h3 class="d-flex align-items-center gap-1 mb-3">{{ plan.planpricing_set.first.price }}<span class="currency"> {{ CURRENCY }}</span> <span class="fs-8 fw-normal">/ {{ _("Per month")}}</span></h3>
<div class="row g-3 mb-7 mb-lg-11">
{% for plan in plan_list %}
<div class="col-lg-3">
<input type="radio" class="btn-check" name="selected_plan" id="plan_{{ forloop.counter }}" value="{{ plan.id }}" autocomplete="off">
<h5 class="mb-4">{{ _("Included")}}</h5>
<ul class="fa-ul ps-1 m-0 pricing">
{% for line in plan.description|splitlines %}
<li class="d-flex align-items-start mb-3">
<span class="fas fa-check text-primary"></span>
<span class="text-body-tertiary fw-semibold">{{ line }}</span>
<label class="btn w-100 h-100 p-0" for="plan_{{ forloop.counter }}">
<div class="card h-100 border border-2 rounded-4 {% if forloop.first %}border-primary{% else %}border-secondary{% endif %}">
<div class="card-body p-4">
<h2 class="mb-4 h5">{{ plan.name }}</h2>
<h3 class="h6 mb-3">
{{ plan.planpricing_set.first.price }}
<span class="currency">{{ CURRENCY }}</span>
<span class="fs-8 fw-normal">/ {{ _("Per month") }}</span>
</h3>
<h5 class="mb-3 h6">{{ _("Included") }}</h5>
<ul class="fa-ul ps-3 m-0">
{% for line in plan.description|splitlines %}
<li class="d-flex align-items-start mb-2">
<span class="fa-li"><i class="fas fa-check text-primary"></i></span>
<span class="text-muted fw-semibold">{{ line }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</ul>
</div>
</div>
</label>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</section>