implement payment with moyasar

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

2
.vscode/launch.json vendored
View File

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

View File

@ -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

View File

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

View File

@ -1,9 +1,12 @@
# management/commands/create_plans.py # 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,
)

View File

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

View File

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

View File

@ -1,5 +1,7 @@
from django.contrib.auth.models import Permission from 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

View File

@ -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}"

View File

@ -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"),

View File

@ -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

View File

@ -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
View File

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

View File

@ -44,7 +44,7 @@
<h6 class="mb-2 text-body-secondary">{% trans 'last login'|capfirst %}</h6> <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}}&nbsp;&nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left}}</small></p>
<div class="d-flex align-items-end mb-md-5 mb-lg-0"> <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>

View File

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

View File

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

View File

@ -1,36 +1,52 @@
{% extends 'base.html' %} {% 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 %}

View File

@ -1,171 +1,204 @@
<div class="container my-5">
<!-- Header Section (unchanged) -->
<div class="d-flex justify-content-between align-items-start mb-5">
{% if logo_url %} {% 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>&nbsp;</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&nbsp;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&nbsp;Amount</span>
</td>
<td style="width: 8%;">
<span class="en">Subtotal&nbsp;with&nbsp;TAX/VAT</span>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
1
</td>
<td class="center">{{ invoice.item_description }}</td>
<td class="number">{{ invoice.unit_price_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="center">{{ invoice.quantity }}</td>
<td class="center"><span class="en">units</span></td>
{% if invoice.rebate %}
<td class="number">{{ invoice.rebate|floatformat:2 }}&nbsp;%</td>
{% endif %}
<td class="number">{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="number">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="en">n/a</span>{% endif %}</td>
<td class="number">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
<td class="number">{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="{% if invoice.rebate %}6{% else %}5{% endif %}" style="background-color: #EEE;"><label><span class="en">Total</span></label> </td>
<td>{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td>{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="en">n/a</span>{% endif %}</td>
<td>{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="en">n/a</span>{% endif %}</td>
<td>{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tfoot>
</table>
<div style="width: 100%;">
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<strong><label><span class="en">Payment</span></label></strong> <label> <span class="en">electronic payment</span></label><br><br>
{% endif %}
<strong><label><span class="en">Payment till</span></label></strong>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}
{% else %}
<label> <span class="en"> paid</span></label>
{% endif %}
{{ invoice.payment_date|date:"Y-m-d" }}
<br><br>
<hr>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}<p><span class="en">This document <strong>is not</strong> an invoice.</span></p> {% endif %}
{% if invoice.tax == None and invoice.is_UE_customer %}
<p>
<span class="en">-Reverse charge.</span>
</p>
{% endif %}
</div> </div>
</div>
<!-- Payment Information (unchanged) -->
<div class="row">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Payment Information</h5>
</div>
<div class="card-body">
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<div class="mb-2">
<span class="text-muted">Method:</span>
<strong>Electronic Payment</strong>
</div>
{% endif %}
<div class="mb-2">
<span class="text-muted">Due Date:</span>
<strong>{{ invoice.payment_date|date:"F j, Y" }}</strong>
</div>
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<div class="alert alert-success p-2 mb-0">
<i class="fas fa-check-circle me-2"></i> Payment Received
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0 fw-semibold">Notes</h5>
</div>
<div class="card-body">
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}
<div class="alert alert-warning p-2 mb-2">
<i class="fas fa-exclamation-triangle me-2"></i> This document is not an invoice.
</div>
{% endif %}
{% if invoice.tax == None and invoice.is_UE_customer %}
<div class="alert alert-info p-2 mb-0">
<i class="fas fa-info-circle me-2"></i> Reverse charge applied.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Footer (unchanged) -->
<div class="mt-5 pt-4 border-top text-center text-muted small">
<p class="mb-1">Thank you for your business!</p>
<p class="mb-0">If you have any questions about this invoice, please contact us.</p>
</div>
</div>
<style>
.sticky-col {
position: sticky;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.table-responsive {
-webkit-overflow-scrolling: touch;
}
</style>

View File

@ -1,89 +1,121 @@
{% load i18n%} {% 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 }}&nbsp;{{ 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&nbsp;{{ 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
View File

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

View File

@ -2,6 +2,14 @@
{% load custom_filters %} {% load 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>