implement payment with moyasar
This commit is contained in:
parent
c01d234e0e
commit
239ea2e66e
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"runserver",
|
||||
"0.0.0.0:8888"
|
||||
"0.0.0.0:8000"
|
||||
],
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
|
||||
@ -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
|
||||
|
||||
18
inventory/management/commands/deactivate_expired_plans.py
Normal file
18
inventory/management/commands/deactivate_expired_plans.py
Normal 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'))
|
||||
@ -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,
|
||||
)
|
||||
|
||||
47
inventory/migrations/0007_paymenthistory.py
Normal file
47
inventory/migrations/0007_paymenthistory.py
Normal 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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0008_alter_vendor_address.py
Normal file
19
inventory/migrations/0008_alter_vendor_address.py
Normal 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,
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
41
scripts/r.py
Normal 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)
|
||||
|
||||
@ -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}} <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>
|
||||
|
||||
39
templates/payment_failed.html
Normal file
39
templates/payment_failed.html
Normal 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 %}
|
||||
40
templates/payment_success.html
Normal file
40
templates/payment_success.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
@ -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> </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 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 Amount</span>
|
||||
</td>
|
||||
<td style="width: 8%;">
|
||||
|
||||
|
||||
<span class="en">Subtotal with 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 }} {{ 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 }} %</td>
|
||||
{% endif %}
|
||||
<td class="number">{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}</td>
|
||||
<td class="number">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }} %{% else %}<span class="en">n/a</span>{% endif %}</td>
|
||||
<td class="number">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
|
||||
<td class="number">{{ invoice.total|floatformat:2 }} {{ 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 }} {{ invoice.currency }}</td>
|
||||
<td>{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }} %{% else %}<span class="en">n/a</span>{% endif %}</td>
|
||||
<td>{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
|
||||
<td>{{ invoice.total|floatformat:2 }} {{ 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>
|
||||
@ -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 }} {{ 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 {{ 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
352
templates/pricing_page.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user