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",
|
"request": "launch",
|
||||||
"args": [
|
"args": [
|
||||||
"runserver",
|
"runserver",
|
||||||
"0.0.0.0:8888"
|
"0.0.0.0:8000"
|
||||||
],
|
],
|
||||||
"django": true,
|
"django": true,
|
||||||
"autoStartBrowser": false,
|
"autoStartBrowser": false,
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
from django.core.cache import cache
|
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 django.contrib.auth.models import Permission
|
||||||
from appointment.models import Service
|
from appointment.models import Service
|
||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from plans.models import PlanPricing
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .models import CustomGroup, Status, Stage
|
from .models import CustomGroup, Status, Stage
|
||||||
from .mixins import AddClassMixin
|
from .mixins import AddClassMixin
|
||||||
@ -462,6 +466,7 @@ class VendorForm(forms.ModelForm):
|
|||||||
:ivar Meta: Inner class to define metadata for the Vendor form.
|
:ivar Meta: Inner class to define metadata for the Vendor form.
|
||||||
:type Meta: Type[VendorForm.Meta]
|
:type Meta: Type[VendorForm.Meta]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Vendor
|
model = Vendor
|
||||||
fields = [
|
fields = [
|
||||||
@ -1427,3 +1432,216 @@ class JournalEntryModelCreateForm(JournalEntryModelCreateFormBase):
|
|||||||
:type bar: int
|
:type bar: int
|
||||||
"""
|
"""
|
||||||
pass
|
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
|
# management/commands/create_plans.py
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from datetime import timedelta
|
||||||
from django.db.models import Q
|
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 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):
|
class Command(BaseCommand):
|
||||||
help = 'Create basic subscription plans structure'
|
help = 'Create basic subscription plans structure'
|
||||||
|
|
||||||
@ -15,10 +18,14 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
Plan.objects.all().delete()
|
# Plan.objects.all().delete()
|
||||||
Quota.objects.all().delete()
|
# Quota.objects.all().delete()
|
||||||
PlanQuota.objects.all().delete()
|
# PlanQuota.objects.all().delete()
|
||||||
Pricing.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')
|
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')
|
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)
|
# PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100)
|
||||||
|
|
||||||
# Define pricing
|
# Define pricing
|
||||||
basic_pricing = Pricing.objects.create(plan=basic_plan, name='Monthly', period=30)
|
basic_pricing = Pricing.objects.create(name='Monthly', period=30)
|
||||||
pro_pricing = Pricing.objects.create(plan=pro_plan, name='Monthly', period=30)
|
pro_pricing = Pricing.objects.create(name='Monthly', period=30)
|
||||||
enterprise_pricing = Pricing.objects.create(plan=enterprise_plan, 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=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=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 django.contrib.auth.models import Permission
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
import hashlib
|
import hashlib
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -24,6 +26,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from appointment.models import StaffMember
|
from appointment.models import StaffMember
|
||||||
from plans.quota import get_user_quota
|
from plans.quota import get_user_quota
|
||||||
|
from plans.models import UserPlan,Quota,PlanQuota
|
||||||
# from plans.models import AbstractPlan
|
# from plans.models import AbstractPlan
|
||||||
# from simple_history.models import HistoricalRecords
|
# from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
@ -910,12 +913,28 @@ class Dealer(models.Model, LocalizedNameMixin):
|
|||||||
objects = DealerUserManager()
|
objects = DealerUserManager()
|
||||||
|
|
||||||
|
|
||||||
# @property
|
@property
|
||||||
# def get_active_plan(self):
|
def active_plan(self):
|
||||||
# try:
|
try:
|
||||||
# return self.user.subscription_set.filter(is_active=True).first()
|
return UserPlan.objects.get(user=self.user,active=True).plan
|
||||||
# except SubscriptionPlan.DoesNotExist:
|
except Exception as e:
|
||||||
# return None
|
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
|
# @property
|
||||||
# def get_plan(self):
|
# def get_plan(self):
|
||||||
@ -1561,7 +1580,7 @@ class Vendor(models.Model, LocalizedNameMixin):
|
|||||||
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
|
||||||
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
|
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
|
||||||
address = models.CharField(
|
address = models.CharField(
|
||||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
max_length=200, verbose_name=_("Address")
|
||||||
)
|
)
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
|
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
|
||||||
@ -1973,4 +1992,100 @@ class DealerSettings(models.Model):
|
|||||||
# db_index=True,
|
# db_index=True,
|
||||||
# unique=False,
|
# unique=False,
|
||||||
# null=True,
|
# 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
|
:return: None
|
||||||
"""
|
"""
|
||||||
if created:
|
if created:
|
||||||
entity = EntityModel.objects.filter(name=instance.dealer.name).first()
|
entity = EntityModel.objects.filter(admin=instance.dealer.user).first()
|
||||||
additionals = to_dict(instance)
|
additionals = to_dict(instance)
|
||||||
vendor = entity.create_vendor(
|
vendor = entity.create_vendor(
|
||||||
vendor_model_kwargs={
|
vendor_model_kwargs={
|
||||||
@ -208,6 +208,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
coa = entity.get_default_coa()
|
coa = entity.get_default_coa()
|
||||||
last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first()
|
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}"
|
# code = f"{int(last_account.code)}{1:03d}"
|
||||||
if len(last_account.code) == 4:
|
if len(last_account.code) == 4:
|
||||||
code = f"{int(last_account.code)}{1:03d}"
|
code = f"{int(last_account.code)}{1:03d}"
|
||||||
|
|||||||
@ -45,6 +45,10 @@ urlpatterns = [
|
|||||||
# ),
|
# ),
|
||||||
# Dashboards
|
# Dashboards
|
||||||
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
|
# 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/<int:pk>/settings/", views.DealerSettingsView, name="dealer_settings"),
|
||||||
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
|
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
|
||||||
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"),
|
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 json
|
||||||
import random
|
|
||||||
import datetime
|
import datetime
|
||||||
from django.shortcuts import redirect
|
from plans.models import AbstractOrder
|
||||||
from django.contrib import messages
|
from django.contrib.auth.models import Group,Permission
|
||||||
from django.utils import timezone
|
from django.db import transaction
|
||||||
from django_ledger.models.entity import UnitOfMeasureModel
|
from django.urls import reverse
|
||||||
from django_ledger.models.journal_entry import JournalEntryModel
|
|
||||||
from django_ledger.models.ledger import LedgerModel
|
|
||||||
from django_ledger.models.transactions import TransactionModel
|
|
||||||
import requests
|
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 inventory import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.items import ItemModel
|
||||||
from django_ledger.models import (
|
from django_ledger.models import (
|
||||||
InvoiceModel,
|
InvoiceModel,
|
||||||
EstimateModel,
|
|
||||||
BillModel,
|
BillModel,
|
||||||
VendorModel,
|
VendorModel,
|
||||||
CustomerModel,
|
|
||||||
ItemTransactionModel,
|
|
||||||
AccountModel
|
|
||||||
)
|
)
|
||||||
from decimal import Decimal
|
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
from appointment.models import StaffMember
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
def get_jwt_token():
|
def get_jwt_token():
|
||||||
"""
|
"""
|
||||||
@ -1262,4 +1258,108 @@ def create_make_accounts(dealer):
|
|||||||
coa_model=coa,
|
coa_model=coa,
|
||||||
balance_type="credit",
|
balance_type="credit",
|
||||||
active=True
|
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 rich import print
|
||||||
from random import randint
|
from random import randint
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from datetime import timedelta
|
||||||
from calendar import month_name
|
from calendar import month_name
|
||||||
from pyzbar.pyzbar import decode
|
from pyzbar.pyzbar import decode
|
||||||
from urllib.parse import urlparse, urlunparse
|
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.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from plans.models import Order,PlanPricing,AbstractOrder,UserPlan,BillingInfo
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
View,
|
View,
|
||||||
ListView,
|
ListView,
|
||||||
@ -155,9 +157,11 @@ from .services import (
|
|||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
CarFinanceCalculator,
|
CarFinanceCalculator,
|
||||||
|
create_user_dealer,
|
||||||
get_car_finance_data,
|
get_car_finance_data,
|
||||||
get_financial_values,
|
get_financial_values,
|
||||||
get_item_transactions,
|
get_item_transactions,
|
||||||
|
handle_payment,
|
||||||
reserve_car,
|
reserve_car,
|
||||||
# send_email,
|
# send_email,
|
||||||
get_user_type,
|
get_user_type,
|
||||||
@ -285,32 +289,11 @@ def dealer_signup(request, *args, **kwargs):
|
|||||||
|
|
||||||
if password != password_confirm:
|
if password != password_confirm:
|
||||||
return JsonResponse({"error": _("Passwords do not match")}, status=400)
|
return JsonResponse({"error": _("Passwords do not match")}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address)
|
||||||
user = User.objects.create(username=email, email=email)
|
return JsonResponse(
|
||||||
user.set_password(password)
|
{"message": _("User created successfully")}, status=200
|
||||||
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
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"error": str(e)}, status=400)
|
return JsonResponse({"error": str(e)}, status=400)
|
||||||
return render(
|
return render(
|
||||||
@ -1816,17 +1799,21 @@ class DealerDetailView(LoginRequiredMixin, DetailView):
|
|||||||
car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer)
|
car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer)
|
||||||
staff_count = dealer.staff_count
|
staff_count = dealer.staff_count
|
||||||
cars_count = models.Car.objects.filter(dealer=dealer).count()
|
cars_count = models.Car.objects.filter(dealer=dealer).count()
|
||||||
quota_dict = get_user_quota(dealer.user)
|
# quota_dict = {}
|
||||||
|
# try:
|
||||||
allowed_users = quota_dict.get("Users", None)
|
# quota_dict = get_user_quota(dealer.user)
|
||||||
allowed_cars = quota_dict.get("Cars", None)
|
# 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["car_makes"] = car_makes
|
||||||
context["staff_count"] = staff_count
|
context["staff_count"] = staff_count
|
||||||
context["cars_count"] = cars_count
|
context["cars_count"] = cars_count
|
||||||
context["allowed_users"] = allowed_users
|
context["allowed_users"] = dealer.user_quota
|
||||||
context["allowed_cars"] = allowed_cars
|
# context["allowed_cars"] = allowed_cars
|
||||||
context["quota_display"] = (
|
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
|
return context
|
||||||
@ -2667,15 +2654,14 @@ class UserCreateView(
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
dealer = get_user_type(self.request)
|
dealer = get_user_type(self.request)
|
||||||
quota_dict = get_user_quota(dealer.user)
|
# quota_dict = get_user_quota(dealer.user)
|
||||||
allowed_users = quota_dict.get("Users")
|
# allowed_users = quota_dict.get("Users")
|
||||||
|
|
||||||
if allowed_users is None:
|
# if allowed_users is None:
|
||||||
messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
|
# messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
|
||||||
return self.form_invalid(form)
|
# return self.form_invalid(form)
|
||||||
|
|
||||||
current_staff_count = dealer.staff.count()
|
if dealer.is_staff_exceed_quota_limit:
|
||||||
if current_staff_count >= allowed_users:
|
|
||||||
messages.error(self.request, _("You have reached the maximum number of staff users allowed for your plan"))
|
messages.error(self.request, _("You have reached the maximum number of staff users allowed for your plan"))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
@ -7655,3 +7641,62 @@ def ledger_unpost_all_journals(request, entity_slug, pk):
|
|||||||
ledger.unpost()
|
ledger.unpost()
|
||||||
ledger.save()
|
ledger.save()
|
||||||
return redirect("journalentry_list", pk=ledger.pk)
|
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>
|
<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>
|
<h4 class="fs-7 text-body-highlight mb-0">{{ dealer.user.last_login|date:"D M d, Y H:i" }}</h4>
|
||||||
</div>
|
</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>
|
<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>
|
<h4 class="fs-7 text-body-highlight mb-0">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -100,13 +100,22 @@
|
|||||||
<div class="mb-5 mb-md-0 mb-lg-5">
|
<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">
|
<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>
|
<h3 class="mb-0 me-2">{{ dealer.user.userplan.plan|capfirst }}</h3>
|
||||||
{% if dealer.user.userplan.active %}
|
{% if dealer.user.userplan %}
|
||||||
<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 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
<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' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n custom_filters%}
|
{% load i18n custom_filters %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container-fluid px-3 px-md-5 py-4">
|
||||||
<!-- Account Details Section -->
|
<!-- Account Details Section -->
|
||||||
<div class="row my-3">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header bg-white border-bottom-0 py-3">
|
||||||
<h5 class="mb-0">{% trans "Your Account" %}</h5>
|
<h5 class="mb-0 fw-semibold">{% trans "Your Account" %}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body pt-0">
|
||||||
<div class="row mb-0">
|
<div class="row g-3">
|
||||||
<div class="col-sm-2">{% trans "Account" %}:</div>
|
<div class="col-md-6 col-lg-3">
|
||||||
<div class="col-sm-10">{{ user.dealer.get_local_name }}</div>
|
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
|
||||||
|
<span class="text-muted small">{% trans "Account" %}</span>
|
||||||
<div class="col-sm-2">{% trans "Status" %}:</div>
|
<span class="fw-semibold">{{ user.dealer.get_local_name }}</span>
|
||||||
<div class="col-sm-10">
|
</div>
|
||||||
{% 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>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2">{% trans "Active until" %}:</div>
|
<div class="col-md-6 col-lg-3">
|
||||||
<div class="col-sm-10">{{ userplan.expire }}</div>
|
<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-md-6 col-lg-3">
|
||||||
<div class="col-sm-10">
|
<div class="d-flex flex-column bg-light rounded-3 p-3 h-100">
|
||||||
{{ userplan.plan }}
|
<span class="text-muted small">{% trans "Active until" %}</span>
|
||||||
<a href="{% url 'upgrade_plan' %}" class="btn btn-sm btn-phoenix-primary ml-2">{% trans "Upgrade" %}</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,21 +57,15 @@
|
|||||||
<!-- Plan Details Section -->
|
<!-- Plan Details Section -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header bg-white border-bottom-0 py-3">
|
||||||
<h5 class="mb-0">{% trans "Plan Details" %}</h5>
|
<h5 class="mb-0 fw-semibold">{% trans "Plan Details" %}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
{% include "plans/plan_table.html" %}
|
||||||
<div class="col-md-8">
|
|
||||||
{% include "plans/plan_table.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
<div class="text-end">
|
||||||
<div style="float:right; text-align: right;">
|
<div class="d-inline-block p-3 bg-light rounded-3">
|
||||||
<h1>
|
<h1 class="h4 mb-1">
|
||||||
<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>
|
<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>
|
</h1>
|
||||||
<h2>{% if not copy %}ORIGINAL{% else %}COPY{% endif %}</h2>
|
<div class="badge bg-{% if copy %}warning{% else %}primary{% endif %} bg-opacity-10 text-{% if copy %}warning{% else %}primary{% endif %} mb-2">
|
||||||
<p> <label> <span class="en">Issued</span></label> {{ invoice.issued|date:"Y-m-d" }}</p>
|
{{ copy|yesno:"COPY,ORIGINAL" }}
|
||||||
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
|
</div>
|
||||||
<p> <label> <span class="en">Date of order</span></label> {{ invoice.selling_date|date:"Y-m-d" }}</p>
|
<div class="d-flex flex-column text-start">
|
||||||
{% else %}
|
<div class="mb-1"><span class="text-muted">Issued:</span> <strong>{{ invoice.issued|date:"F j, Y" }}</strong></div>
|
||||||
<p> </p>
|
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
|
||||||
{% endif %}
|
<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>
|
</div>
|
||||||
|
|
||||||
<table style="width: 100%; margin-bottom: 40px; font-size: 12px;" >
|
<div class="col-md-6">
|
||||||
<tr>
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<td style="width: 50%;">
|
<div class="card-header bg-light">
|
||||||
</td>
|
<h5 class="mb-0 fw-semibold">Buyer</h5>
|
||||||
<td style="width: 50%; padding-right: 4em; font-weight: bold; font-size: 15px;" id="shipping">
|
</div>
|
||||||
<strong> <label><span class="en">Shipping address</span></label></strong><br><br>
|
<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>
|
<!-- Shipping Address (unchanged) -->
|
||||||
{{ invoice.shipping_street }}<br>
|
{% if invoice.shipping_name %}
|
||||||
{{ invoice.shipping_zipcode }} {{ invoice.shipping_city }}<br>
|
<div class="card border-0 shadow-sm mb-5">
|
||||||
{{ invoice.buyer_country.code }} - {{ invoice.buyer_country.name }}
|
<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 %}
|
||||||
|
|
||||||
|
<!-- Items Table - Now with horizontal scrolling -->
|
||||||
</td>
|
<div class="card border-0 shadow-sm mb-5">
|
||||||
</tr>
|
<div class="card-header bg-light">
|
||||||
<tr>
|
<h5 class="mb-0 fw-semibold">Invoice Items</h5>
|
||||||
<td style="width: 50%; vertical-align: top;">
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
<strong> <label><span class="en">Seller</span></label></strong><br><br>
|
<div class="table-responsive" style="overflow-x: auto;">
|
||||||
{{ invoice.issuer_name }}<br>
|
<div style="min-width: 800px;"> <!-- Minimum width to ensure scrolling on smaller screens -->
|
||||||
{{ invoice.issuer_street }}<br>
|
<table class="table table-hover mb-0">
|
||||||
{{ invoice.issuer_zipcode }} {{ invoice.issuer_city}}<br>
|
<thead class="table-light">
|
||||||
{{ invoice.issuer_country.code }} - {{ invoice.issuer_country.name }}<p>
|
<tr>
|
||||||
<label><span class="en">VAT ID</span></label> {{ invoice.issuer_tax_number }}<br>
|
<th class="text-center sticky-col" style="width: 5%; left: 0; background-color: #f8f9fa; z-index: 1;">#</th>
|
||||||
</td>
|
<th style="width: 25%; min-width: 200px;">Description</th>
|
||||||
<td style="width: 50%; vertical-align: top;">
|
<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>
|
||||||
<strong> <label> <span class="en">Buyer</span></label></strong><br><br>
|
<th class="text-center" style="width: 8%; min-width: 80px;">Unit</th>
|
||||||
{{ invoice.buyer_name }}<br>
|
{% if invoice.rebate %}
|
||||||
{{ invoice.buyer_street }}<br>
|
<th class="text-center" style="width: 8%; min-width: 80px;">Rebate</th>
|
||||||
{{ 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<th class="text-end" style="width: 10%; min-width: 100px;">Subtotal</th>
|
||||||
</td>
|
<th class="text-center" style="width: 8%; min-width: 80px;">VAT</th>
|
||||||
</tr>
|
<th class="text-end" style="width: 10%; min-width: 100px;">VAT Amount</th>
|
||||||
</table>
|
<th class="text-end" style="width: 10%; min-width: 100px;">Total</th>
|
||||||
|
</tr>
|
||||||
<table style="margin-bottom: 40px; width: 100%;" id="items">
|
</thead>
|
||||||
<thead>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="text-center sticky-col" style="left: 0; background-color: white; z-index: 1;">1</td>
|
||||||
|
<td>{{ invoice.item_description }}</td>
|
||||||
</td>
|
<td class="text-end">{{ invoice.unit_price_net|floatformat:2 }} {{ invoice.currency }}</td>
|
||||||
<td>
|
<td class="text-center">{{ invoice.quantity }}</td>
|
||||||
|
<td class="text-center">units</td>
|
||||||
<span class="en">Description</span>
|
{% if invoice.rebate %}
|
||||||
|
<td class="text-center">{{ invoice.rebate|floatformat:2 }}%</td>
|
||||||
</td>
|
{% endif %}
|
||||||
|
<td class="text-end">{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}</td>
|
||||||
<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>
|
||||||
<span class="en">Unit price</span>
|
<td class="text-end fw-bold">{{ invoice.total|floatformat:2 }} {{ invoice.currency }}</td>
|
||||||
|
</tr>
|
||||||
</td>
|
</tbody>
|
||||||
<td>
|
<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>
|
||||||
<span class="en">Qty.</span>
|
<td class="text-end fw-bold">{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}</td>
|
||||||
</td>
|
<td class="text-center fw-bold">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}%{% else %}n/a{% endif %}</td>
|
||||||
<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>
|
||||||
</td>
|
</tr>
|
||||||
{% if invoice.rebate %}
|
</tfoot>
|
||||||
|
</table>
|
||||||
<td>
|
</div>
|
||||||
<span class="en">Rebate</span>
|
</div>
|
||||||
|
|
||||||
</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 %}
|
|
||||||
|
|
||||||
|
|
||||||
</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%}
|
{% load i18n %}
|
||||||
<div class="table-responsive mt-4">
|
<div class="table-responsive">
|
||||||
<table class="plan_table table border-top border-translucent fs-9 mb-4">
|
<table class="table table-hover table-compare mb-0">
|
||||||
<thead class="">
|
<thead>
|
||||||
<tr>
|
<tr class="border-bottom border-200">
|
||||||
<th scope="col"></th>
|
<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 %}
|
{% for plan in plan_list %}
|
||||||
<th scope="col" class="plan_header text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
|
<th class="text-center py-3 {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}" style="width: calc(70%/{{ plan_list|length }});">
|
||||||
{% if plan.url %}<a href="{{ plan.url }}" class="info_link plan text-decoration-none">{% endif %}
|
<div class="d-flex flex-column align-items-center">
|
||||||
<span class="plan_name font-weight-bold">{{ plan.name }}</span>
|
{% if plan.url %}<a href="{{ plan.url }}" class="text-decoration-none text-dark">{% endif %}
|
||||||
{% if plan == userplan.plan %}
|
<h6 class="mb-1 fw-bold">{{ plan.name }}</h6>
|
||||||
<span class="current current_plan badge badge-phoenix badge-phoenix-success">{% trans "Current Plan" %}</span>
|
{% if plan == userplan.plan %}
|
||||||
{% endif %}
|
<span class="badge bg-success bg-opacity-10 text-success mt-1">{% trans "Current Plan" %}</span>
|
||||||
|
{% endif %}
|
||||||
{% if plan.url %}</a>{% endif %}
|
{% if plan.url %}</a>{% endif %}
|
||||||
</th>
|
</div>
|
||||||
|
</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for quota_row in plan_table %}
|
{% for quota_row in plan_table %}
|
||||||
<tr class="quota_row">
|
<tr class="border-bottom border-200 {% cycle 'bg-white' 'bg-light' %}">
|
||||||
<th scope="row" class="quota_header align-middle">
|
<th class="ps-4 fw-normal position-sticky start-0 bg-inherit border-end border-200">
|
||||||
{% if quota_row.0.url %}<a href="{{ quota_row.0.url }}" class="info_link quota text-decoration-none">{% endif %}
|
<div class="d-flex flex-column py-2">
|
||||||
<span class="quota_name fw-bold">{{ quota_row.0.name }}</span>
|
{% if quota_row.0.url %}<a href="{{ quota_row.0.url }}" class="text-decoration-none text-dark">{% endif %}
|
||||||
<small class="quota_description d-block text-muted">{{ quota_row.0.description }}</small>
|
<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 %}
|
{% if quota_row.0.url %}</a>{% endif %}
|
||||||
</th>
|
</div>
|
||||||
{% for plan_quota in quota_row.1 %}
|
</th>
|
||||||
<td class="align-middle text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
|
{% for plan_quota in quota_row.1 %}
|
||||||
{% if plan_quota != None %}
|
<td class="text-center py-2 align-middle {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}">
|
||||||
{% if quota_row.0.is_boolean %}
|
{% if plan_quota != None %}
|
||||||
{% if plan_quota.value %}<i class="fas fa-check text-success"></i>{% else %}<i class="fas fa-times text-danger"></i>{% endif %}
|
{% if quota_row.0.is_boolean %}
|
||||||
{% else %}
|
{% if plan_quota.value %}
|
||||||
{% if plan_quota.value == None %}{% trans 'No Limit' %}{% else %}{{ plan_quota.value }} {{ quota_row.0.unit }}{% endif %}
|
<i class="fas fa-check-circle text-success fs-6"></i>
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
<i class="fas fa-times-circle text-danger fs-6"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
{% else %}
|
||||||
{% endfor %}
|
<span class="d-block">
|
||||||
</tr>
|
{% 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 %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot class="">
|
<tfoot>
|
||||||
<tr>
|
<tr class="border-bottom border-200">
|
||||||
<th scope="col"></th>
|
<th class="ps-4 bg-white position-sticky start-0 border-end border-200"></th>
|
||||||
<th colspan="{{ plan_list|length }}" class="text-center font-weight-bold py-3">{% trans 'Pricing' %}</th>
|
<th colspan="{{ plan_list|length }}" class="text-center py-3 bg-light">
|
||||||
|
<h6 class="mb-0 fw-bold">{% trans 'Pricing' %}</h6>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<tr>
|
<tr class="border-bottom border-200">
|
||||||
<th scope="col"></th>
|
<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 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>
|
|
||||||
{% for plan in plan_list %}
|
{% for plan in plan_list %}
|
||||||
<th class="planpricing_footer text-center {% if forloop.counter0 == current_userplan_index %}current bg-body-highlight{% endif %}">
|
<td class="text-center py-3 {% if forloop.counter0 == current_userplan_index %}bg-primary-soft{% endif %}">
|
||||||
{% if plan.available %}
|
{% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %}
|
||||||
<ul class="list-unstyled">
|
<a href="{% url 'create_order_plan_change' pk=plan.id %}" class="btn btn-sm btn-outline-primary px-3">
|
||||||
{% if not plan.is_free %}
|
{% trans "Change" %}
|
||||||
{% for plan_pricing in plan.planpricing_set.all %}
|
</a>
|
||||||
{% if plan_pricing.visible %}
|
{% endif %}
|
||||||
<li class="mb-2">
|
</td>
|
||||||
{% if plan_pricing.pricing.url %}<a href="{{ plan_pricing.pricing.url }}" class="info_link pricing text-decoration-none">{% endif %}
|
{% endfor %}
|
||||||
<span class="plan_pricing_name font-weight-bold">{{ plan_pricing.pricing.name }}</span>
|
</tr>
|
||||||
<small class="plan_pricing_period d-block text-muted">({{ plan_pricing.pricing.period }} {% trans "day" %})</small>
|
{% endif %}
|
||||||
{% if plan_pricing.pricing.url %}</a>{% endif %}
|
|
||||||
<span class="plan_pricing_price d-block font-weight-bold">{{ plan_pricing.price }} {{ CURRENCY }}</span>
|
<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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</li>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
{% else %}
|
{% endfor %}
|
||||||
<li class="mb-2">
|
{% else %}
|
||||||
<span class="plan_pricing_name font-weight-bold">{% trans "Free" %}</span>
|
<div class="p-3 rounded-2 bg-light">
|
||||||
<small class="plan_pricing_period d-block text-muted">({% trans "no expiry" %})</small>
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<span class="plan_pricing_price d-block font-weight-bold">0 {{ CURRENCY }}</span>
|
<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 %}
|
{% 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 %}
|
{% if userplan.is_expired %}
|
||||||
{% trans "Select" %}
|
{% trans "Select" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -91,21 +123,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</ul>
|
{% endif %}
|
||||||
{% else %}
|
</div>
|
||||||
<span class="plan_not_available text-muted">
|
{% else %}
|
||||||
{% url 'upgrade_plan' as upgrade_url %}
|
<div class="alert alert-warning bg-warning-soft border-0 small mb-0">
|
||||||
{% blocktrans %}
|
{% url 'upgrade_plan' as upgrade_url %}
|
||||||
This plan is not available anymore and cannot be extended.<br>
|
{% blocktrans %}
|
||||||
You need to upgrade your account to any of <a href="{{ upgrade_url }}" class="text-primary">currently available plans</a>.
|
<p class="mb-1">This plan is no longer available.</p>
|
||||||
{% endblocktrans %}
|
<a href="{{ upgrade_url }}" class="fw-bold text-warning">Upgrade to current plans →</a>
|
||||||
</span>
|
{% endblocktrans %}
|
||||||
{% endif %}
|
</div>
|
||||||
</th>
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</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 custom_filters %}
|
||||||
{% load i18n static %}
|
{% 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 %}
|
{% 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="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">
|
<div class="container-medium position-relative">
|
||||||
<h2 class="mb-7">{{ _("Pricing") }}</h2>
|
<h2 class="mb-7">{{ _("Pricing") }}</h2>
|
||||||
<div class="row g-3 mb-7 mb-lg-11">
|
<div class="row g-3 mb-7 mb-lg-11">
|
||||||
{% for plan in plan_list %}
|
{% for plan in plan_list %}
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
<div class="pricing-card h-100">
|
<input type="radio" class="btn-check" name="selected_plan" id="plan_{{ forloop.counter }}" value="{{ plan.id }}" autocomplete="off">
|
||||||
<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>
|
|
||||||
|
|
||||||
<h5 class="mb-4">{{ _("Included")}}</h5>
|
<label class="btn w-100 h-100 p-0" for="plan_{{ forloop.counter }}">
|
||||||
<ul class="fa-ul ps-1 m-0 pricing">
|
<div class="card h-100 border border-2 rounded-4 {% if forloop.first %}border-primary{% else %}border-secondary{% endif %}">
|
||||||
{% for line in plan.description|splitlines %}
|
<div class="card-body p-4">
|
||||||
<li class="d-flex align-items-start mb-3">
|
<h2 class="mb-4 h5">{{ plan.name }}</h2>
|
||||||
<span class="fas fa-check text-primary"></span>
|
<h3 class="h6 mb-3">
|
||||||
<span class="text-body-tertiary fw-semibold">{{ line }}</span>
|
{{ 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>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user