diff --git a/.vscode/launch.json b/.vscode/launch.json index 998f9ee1..8e4ea5ba 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "args": [ "runserver", - "0.0.0.0:8888" + "0.0.0.0:8000" ], "django": true, "autoStartBrowser": false, diff --git a/inventory/forms.py b/inventory/forms.py index 1cc9586e..5132e364 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -1,9 +1,13 @@ from django.core.cache import cache +from datetime import datetime +from luhnchecker.luhn import Luhn +from django.core.validators import RegexValidator from django.contrib.auth.models import Permission from appointment.models import Service from phonenumber_field.formfields import PhoneNumberField from django.core.validators import MinLengthValidator from django import forms +from plans.models import PlanPricing from django.contrib.auth import get_user_model from .models import CustomGroup, Status, Stage from .mixins import AddClassMixin @@ -462,6 +466,7 @@ class VendorForm(forms.ModelForm): :ivar Meta: Inner class to define metadata for the Vendor form. :type Meta: Type[VendorForm.Meta] """ + class Meta: model = Vendor fields = [ @@ -1427,3 +1432,216 @@ class JournalEntryModelCreateForm(JournalEntryModelCreateFormBase): :type bar: int """ pass + + +class PlanPricingForm(forms.ModelForm): + """ + Represents a form for managing plan pricing. + + This class provides a form for creating or updating `PlanPricing` instances. + It automatically includes all fields defined in the `PlanPricing` model and + can be used within a Django web application. + + :ivar model: Associated model for the form. + :type model: PlanPricing + :ivar fields: Fields to include in the form. The value "__all__" indicates + that all model fields should be included. + :type fields: str + """ + class Meta: + model = PlanPricing + fields = ["plan","pricing", "price"] + + + +class CreditCardField(forms.CharField): + def clean(self, value): + value = super().clean(value) + if value: + # Remove all non-digit characters + cleaned_value = ''.join(c for c in value if c.isdigit()) + + # Validate using Luhn algorithm + if not Luhn.check_luhn(cleaned_value): + raise forms.ValidationError("Please enter a valid credit card number") + + # Add basic card type detection (optional) + if cleaned_value.startswith('4'): + self.card_type = 'visa' + elif cleaned_value.startswith(('51', '52', '53', '54', '55')): + self.card_type = 'mastercard' + elif cleaned_value.startswith(('34', '37')): + self.card_type = 'amex' + else: + self.card_type = 'unknown' + + return value + return value + +class ExpiryDateField(forms.CharField): + def clean(self, value): + value = super().clean(value) + if value: + try: + month, year = value.split('/') + month = int(month.strip()) + year = int(year.strip()) + + # Handle 2-digit year + if year < 100: + year += 2000 + + # Validate month + if month < 1 or month > 12: + raise forms.ValidationError("Please enter a valid month (01-12)") + + # Validate not expired + current_year = datetime.now().year + current_month = datetime.now().month + + if year < current_year or (year == current_year and month < current_month): + raise forms.ValidationError("This card appears to be expired") + + except (ValueError, AttributeError): + raise forms.ValidationError("Please enter a valid expiry date in MM/YY format") + + return value + +class CVVField(forms.CharField): + def clean(self, value): + value = super().clean(value) + if value: + if not value.isdigit(): + raise forms.ValidationError("CVV must contain only digits") + if len(value) not in (3, 4): + raise forms.ValidationError("CVV must be 3 or 4 digits") + return value + +class PaymentPlanForm(forms.Form): + # Customer Information + first_name = forms.CharField( + max_length=100, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': _('Your First Name'), + 'id': 'first-name' + }), + label=_('First Name') + ) + + last_name = forms.CharField( + max_length=100, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': _('Your Last Name'), + 'id': 'last-name' + }), + label=_('Last Name') + ) + + email = forms.EmailField( + widget=forms.EmailInput(attrs={ + 'class': 'form-control', + 'placeholder': _('Your Email'), + 'id': 'email' + }), + label=_('Email Address') + ) + + phone = forms.CharField( + required=False, + max_length=20, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': _('Your Phone Number'), + 'id': 'phone' + }), + label=_('Phone Number'), + validators=[RegexValidator( + regex=r'^\+?[0-9]{8,15}$', + message=_('Enter a valid phone number (8-15 digits, + optional)') + )] + ) + + # Credit Card Fields (not saved to database) + card_number = CreditCardField( + required=True, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': '1234 5678 9012 3456', + 'id': 'card-number', + + }), + label="Card Number" + ) + + expiry_date = ExpiryDateField( + required=True, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'MM/YY', + 'id': 'expiry', + + }), + label="Expiration Date" + ) + + cvv = CVVField( + required=True, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': '123', + 'id': 'cvv', + + }), + label="Security Code (CVV)" + ) + + card_name = forms.CharField( + required=True, + max_length=100, + widget=forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'John Doe', + 'id': 'card-name', + + }), + label="Name on Card" + ) + + # Terms and conditions + terms = forms.BooleanField( + required=True, + widget=forms.CheckboxInput(attrs={ + 'class': 'form-check-input', + 'id': 'terms' + }), + label=_('I agree to the Terms and Conditions'), + error_messages={ + 'required': _('You must accept the terms and conditions') + } + ) + def clean(self): + cleaned_data = super().clean() + payment_method = self.data.get('payment-method') # From your radio buttons + + if payment_method == 'credit-card': + if not all([ + cleaned_data.get('card_number'), + cleaned_data.get('expiry_date'), + cleaned_data.get('cvv'), + cleaned_data.get('card_name') + ]): + raise forms.ValidationError("Please complete all credit card fields") + + return cleaned_data + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + if user and user.is_authenticated: + # Pre-fill form with user data if available + self.fields['first_name'].initial = user.first_name + self.fields['last_name'].initial = user.last_name + self.fields['email'].initial = user.email diff --git a/inventory/management/commands/deactivate_expired_plans.py b/inventory/management/commands/deactivate_expired_plans.py new file mode 100644 index 00000000..4087e158 --- /dev/null +++ b/inventory/management/commands/deactivate_expired_plans.py @@ -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')) \ No newline at end of file diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index e28c3139..714caf25 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -1,9 +1,12 @@ # management/commands/create_plans.py from decimal import Decimal +from datetime import timedelta from django.db.models import Q +from django.utils import timezone +from plans.quota import get_user_quota +from django.contrib.auth.models import User from django.core.management.base import BaseCommand -from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing - +from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing,UserPlan,Order,BillingInfo,AbstractOrder class Command(BaseCommand): help = 'Create basic subscription plans structure' @@ -15,10 +18,14 @@ class Command(BaseCommand): ) def handle(self, *args, **options): - Plan.objects.all().delete() - Quota.objects.all().delete() - PlanQuota.objects.all().delete() - Pricing.objects.all().delete() + # Plan.objects.all().delete() + # Quota.objects.all().delete() + # PlanQuota.objects.all().delete() + # Pricing.objects.all().delete() + # PlanPricing.objects.all().delete() + # UserPlan.objects.all().delete() + # Order.objects.all().delete() + # BillingInfo.objects.all().delete() three_users_quota = Quota.objects.create(name='3 users', codename='3 users', unit='number') five_users_quota = Quota.objects.create(name='5 users', codename='5 users', unit='number') @@ -38,10 +45,63 @@ class Command(BaseCommand): # PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100) # Define pricing - basic_pricing = Pricing.objects.create(plan=basic_plan, name='Monthly', period=30) - pro_pricing = Pricing.objects.create(plan=pro_plan, name='Monthly', period=30) - enterprise_pricing = Pricing.objects.create(plan=enterprise_plan, name='Monthly', period=30) + basic_pricing = Pricing.objects.create(name='Monthly', period=30) + pro_pricing = Pricing.objects.create(name='Monthly', period=30) + enterprise_pricing = Pricing.objects.create(name='Monthly', period=30) PlanPricing.objects.create(plan=basic_plan, pricing=basic_pricing, price=Decimal('9.99')) PlanPricing.objects.create(plan=pro_plan, pricing=pro_pricing, price=Decimal('19.99')) - PlanPricing.objects.create(plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal('29.99')) \ No newline at end of file + 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, + ) diff --git a/inventory/migrations/0007_paymenthistory.py b/inventory/migrations/0007_paymenthistory.py new file mode 100644 index 00000000..3fead346 --- /dev/null +++ b/inventory/migrations/0007_paymenthistory.py @@ -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')], + }, + ), + ] diff --git a/inventory/migrations/0008_alter_vendor_address.py b/inventory/migrations/0008_alter_vendor_address.py new file mode 100644 index 00000000..226fd8ab --- /dev/null +++ b/inventory/migrations/0008_alter_vendor_address.py @@ -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, + ), + ] diff --git a/inventory/models.py b/inventory/models.py index ea1eb17e..3f7eb340 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1,5 +1,7 @@ from django.contrib.auth.models import Permission from decimal import Decimal +from django.utils import timezone +from django.core.validators import MinValueValidator, MaxValueValidator import hashlib from django.db import models from datetime import timedelta @@ -24,6 +26,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from appointment.models import StaffMember from plans.quota import get_user_quota +from plans.models import UserPlan,Quota,PlanQuota # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords @@ -910,12 +913,28 @@ class Dealer(models.Model, LocalizedNameMixin): objects = DealerUserManager() - # @property - # def get_active_plan(self): - # try: - # return self.user.subscription_set.filter(is_active=True).first() - # except SubscriptionPlan.DoesNotExist: - # return None + @property + def active_plan(self): + try: + return UserPlan.objects.get(user=self.user,active=True).plan + except Exception as e: + print(e) + return None + @property + def user_quota(self): + try: + return PlanQuota.objects.get(plan=self.active_plan).value + except Exception as e: + print(e) + return None + + @property + def is_staff_exceed_quota_limit(self): + quota = self.user_quota + staff_count = self.staff.count() + if staff_count >= quota: + return True + return False # # @property # def get_plan(self): @@ -1561,7 +1580,7 @@ class Vendor(models.Model, LocalizedNameMixin): phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField( - max_length=200, blank=True, null=True, verbose_name=_("Address") + max_length=200, verbose_name=_("Address") ) logo = models.ImageField( upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") @@ -1973,4 +1992,100 @@ class DealerSettings(models.Model): # db_index=True, # unique=False, # null=True, -# ) \ No newline at end of file +# ) + + +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 diff --git a/inventory/signals.py b/inventory/signals.py index 53e6774c..67a1ab72 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -189,7 +189,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs): :return: None """ if created: - entity = EntityModel.objects.filter(name=instance.dealer.name).first() + entity = EntityModel.objects.filter(admin=instance.dealer.user).first() additionals = to_dict(instance) vendor = entity.create_vendor( vendor_model_kwargs={ @@ -208,6 +208,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs): coa = entity.get_default_coa() last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first() + # code = f"{int(last_account.code)}{1:03d}" if len(last_account.code) == 4: code = f"{int(last_account.code)}{1:03d}" diff --git a/inventory/urls.py b/inventory/urls.py index c994084e..30d64030 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -45,6 +45,10 @@ urlpatterns = [ # ), # Dashboards # path("user//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//settings/", views.DealerSettingsView, name="dealer_settings"), path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"), path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"), diff --git a/inventory/utils.py b/inventory/utils.py index 593354a6..aa71f067 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1,35 +1,31 @@ -from django_ledger.io import roles -from django.core.exceptions import ObjectDoesNotExist import json -import random import datetime -from django.shortcuts import redirect -from django.contrib import messages -from django.utils import timezone -from django_ledger.models.entity import UnitOfMeasureModel -from django_ledger.models.journal_entry import JournalEntryModel -from django_ledger.models.ledger import LedgerModel -from django_ledger.models.transactions import TransactionModel +from plans.models import AbstractOrder +from django.contrib.auth.models import Group,Permission +from django.db import transaction +from django.urls import reverse import requests +from decimal import Decimal +from django.utils import timezone +from django_ledger.io import roles +from django.contrib import messages +from django.shortcuts import redirect +from django.core.exceptions import ObjectDoesNotExist +from django_ledger.models.journal_entry import JournalEntryModel +from django_ledger.models.transactions import TransactionModel from inventory import models from django.conf import settings from django.core.mail import send_mail from django.utils.translation import gettext_lazy as _ -from inventory.utilities.financials import get_financial_value from django_ledger.models.items import ItemModel from django_ledger.models import ( InvoiceModel, - EstimateModel, BillModel, VendorModel, - CustomerModel, - ItemTransactionModel, - AccountModel ) -from decimal import Decimal from django.utils.translation import get_language - - +from appointment.models import StaffMember +from django.contrib.auth.models import User def get_jwt_token(): """ @@ -1262,4 +1258,108 @@ def create_make_accounts(dealer): coa_model=coa, balance_type="credit", active=True - ) \ No newline at end of file + ) + + +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 diff --git a/inventory/views.py b/inventory/views.py index 4e66b8f4..173c6b00 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -7,6 +7,7 @@ import numpy as np # from rich import print from random import randint from decimal import Decimal +from datetime import timedelta from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -44,6 +45,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.decorators import permission_required from django.shortcuts import render, get_object_or_404, redirect +from plans.models import Order,PlanPricing,AbstractOrder,UserPlan,BillingInfo from django.views.generic import ( View, ListView, @@ -155,9 +157,11 @@ from .services import ( ) from .utils import ( CarFinanceCalculator, + create_user_dealer, get_car_finance_data, get_financial_values, get_item_transactions, + handle_payment, reserve_car, # send_email, get_user_type, @@ -285,32 +289,11 @@ def dealer_signup(request, *args, **kwargs): if password != password_confirm: return JsonResponse({"error": _("Passwords do not match")}, status=400) - try: - with transaction.atomic(): - user = User.objects.create(username=email, email=email) - user.set_password(password) - user.save() - group = Group.objects.create(name=f"{user.pk}-Admin") - user.groups.add(group) - for perm in Permission.objects.filter( - content_type__app_label__in=["inventory", "django_ledger"] - ): - group.permissions.add(perm) - - StaffMember.objects.create(user=user) - models.Dealer.objects.create( - user=user, - name=name, - arabic_name=arabic_name, - crn=crn, - vrn=vrn, - phone_number=phone, - address=address, - ) - return JsonResponse( - {"message": _("User created successfully")}, status=200 - ) + create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address) + return JsonResponse( + {"message": _("User created successfully")}, status=200 + ) except Exception as e: return JsonResponse({"error": str(e)}, status=400) return render( @@ -1816,17 +1799,21 @@ class DealerDetailView(LoginRequiredMixin, DetailView): car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer) staff_count = dealer.staff_count cars_count = models.Car.objects.filter(dealer=dealer).count() - quota_dict = get_user_quota(dealer.user) - - allowed_users = quota_dict.get("Users", None) - allowed_cars = quota_dict.get("Cars", None) + # quota_dict = {} + # try: + # quota_dict = get_user_quota(dealer.user) + # except Exception as e: + # print(e) + # allowed_users = quota_dict.get("Users", None) + # allowed_cars = quota_dict.get("Cars", None) + user_quota = dealer.user_quota context["car_makes"] = car_makes context["staff_count"] = staff_count context["cars_count"] = cars_count - context["allowed_users"] = allowed_users - context["allowed_cars"] = allowed_cars + context["allowed_users"] = dealer.user_quota + # context["allowed_cars"] = allowed_cars context["quota_display"] = ( - f"{staff_count}/{allowed_users}" if allowed_users is not None else "N/A" + f"{staff_count}/{user_quota}" if user_quota else "0" ) return context @@ -2667,15 +2654,14 @@ class UserCreateView( def form_valid(self, form): dealer = get_user_type(self.request) - quota_dict = get_user_quota(dealer.user) - allowed_users = quota_dict.get("Users") + # quota_dict = get_user_quota(dealer.user) + # allowed_users = quota_dict.get("Users") - if allowed_users is None: - messages.error(self.request, _("The user quota for staff members is not defined. Please contact support")) - return self.form_invalid(form) + # if allowed_users is None: + # messages.error(self.request, _("The user quota for staff members is not defined. Please contact support")) + # return self.form_invalid(form) - current_staff_count = dealer.staff.count() - if current_staff_count >= allowed_users: + if dealer.is_staff_exceed_quota_limit: messages.error(self.request, _("You have reached the maximum number of staff users allowed for your plan")) return self.form_invalid(form) @@ -7655,3 +7641,62 @@ def ledger_unpost_all_journals(request, entity_slug, pk): ledger.unpost() ledger.save() return redirect("journalentry_list", pk=ledger.pk) + + +def pricing_page(request): + plan_list = PlanPricing.objects.all() + form = forms.PaymentPlanForm() + return render(request, "pricing_page.html", {"plan_list": plan_list, "CURRENCY": "$","form":form}) + +# @require_POST +def submit_plan(request): + selected_plan_id = request.POST.get("selected_plan") + pp = PlanPricing.objects.get(pk=selected_plan_id) + + order = Order.objects.create( + user=request.user, + plan=pp.plan, + pricing=pp.pricing, + amount=pp.price, + currency="SAR", + tax=15, + status=AbstractOrder.STATUS.NEW + ) + transaction_url = handle_payment(request,order) + return redirect(transaction_url) + +def payment_callback(request): + payment_id = request.GET.get("id") + history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() + payment_status = request.GET.get("status") + order = Order.objects.filter(user=request.user,status=AbstractOrder.STATUS.NEW).first() + + if payment_status == "paid": + billing_info,created = BillingInfo.objects.get_or_create( + user=request.user, + tax_number='123456789', + name='', + street='', + zipcode='12345', + city='Riyadh', + country='KSA', + ) + if created: + userplan =UserPlan.objects.create( + user=request.user, + plan=order.plan, + active=True, + ) + userplan.initialize() + + order.complete_order() + history.status = "paid" + history.save() + invoice = order.get_invoices().first() + return render(request, "payment_success.html",{"order":order,"invoice":invoice}) + + elif payment_status == "failed": + history.status = "failed" + history.save() + message = request.GET.get('message') + return render(request, "payment_failed.html", {"message": message}) diff --git a/scripts/r.py b/scripts/r.py new file mode 100644 index 00000000..3e3f03fc --- /dev/null +++ b/scripts/r.py @@ -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) + diff --git a/templates/dealers/dealer_detail.html b/templates/dealers/dealer_detail.html index d62b1aba..8c34e416 100644 --- a/templates/dealers/dealer_detail.html +++ b/templates/dealers/dealer_detail.html @@ -44,7 +44,7 @@
{% trans 'last login'|capfirst %}

{{ dealer.user.last_login|date:"D M d, Y H:i" }}

-
+
{% trans 'Total users'|capfirst %}

{{ dealer.staff_count }} / {{ allowed_users }}

@@ -100,13 +100,22 @@

{{ dealer.user.userplan.plan|capfirst }}

- {% if dealer.user.userplan.active %} - {% trans 'Active' %} + {% if dealer.user.userplan %} + {% if not dealer.user.userplan.is_expired %} + {% trans 'Active' %} + {% else %} + {% trans 'Expired' %} + {{ _("Renew") }} + {% endif %} + {% if dealer.user.userplan.plan.name != "Enterprise" %} + {{ _("Upgrade") }} + {% endif %} {% else %} - {% trans 'Expired' %} + You have no active plan. {{ _("Subscribe") }} {% endif %} +
-

{% trans 'Active until' %}: {{ dealer.user.userplan.expire}}

+

{% trans 'Active until' %}: {{ dealer.user.userplan.expire}}   {% trans 'Days left' %}: {{ dealer.user.userplan.days_left}}

{{ dealer.user.userplan.plan.planpricing_set.first.price }} {{ CURRENCY }}

{{ _("Per month")}}
diff --git a/templates/payment_failed.html b/templates/payment_failed.html new file mode 100644 index 00000000..3c71fcf6 --- /dev/null +++ b/templates/payment_failed.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block content %} +
+ + +
+
+

{% trans "Payment Failed" %}

+ +
+
+ + +
+
+
+ +

{% trans "Payment Failed"%}

+ {% if message %} +

{{message}}.

+ {% else %} +

{% trans "We couldn't process your payment. Please try again"%}.

+ {% endif %} + + {% trans "Back to Home"%} + +
+
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/templates/payment_success.html b/templates/payment_success.html new file mode 100644 index 00000000..eccb5568 --- /dev/null +++ b/templates/payment_success.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block content %} +
+ + +
+
+

{% trans "Payment Successful"%}

+ +
+
+ + +
+
+
+ +

Thank You!

+

Your payment was successful. Your order is being processed.

+ {% if invoice %} + + View Invoice + + {% endif %} + + Back to Home + +
+
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/templates/plans/current.html b/templates/plans/current.html index f987ab58..e796bd06 100644 --- a/templates/plans/current.html +++ b/templates/plans/current.html @@ -1,36 +1,52 @@ {% extends 'base.html' %} -{% load i18n custom_filters%} +{% load i18n custom_filters %} {% block content %} - +
-
+
-
-
-
{% trans "Your Account" %}
+
+
+
{% trans "Your Account" %}
-
-
-
{% trans "Account" %}:
-
{{ user.dealer.get_local_name }}
- -
{% trans "Status" %}:
-
- {% if userplan.active %} - {% trans "Active" %} - {% else %} - {% trans "Expired" %} - {% endif %} +
+
+
+
+ {% trans "Account" %} + {{ user.dealer.get_local_name }} +
-
{% trans "Active until" %}:
-
{{ userplan.expire }}
+
+
+ {% trans "Status" %} + {% if userplan.active %} + {% trans "Active" %} + {% else %} + {% trans "Expired" %} + {% endif %} +
+
-
{% trans "Plan" %}:
-
- {{ userplan.plan }} - {% trans "Upgrade" %} +
+
+ {% trans "Active until" %} + {{ userplan.expire }} +
+
+ +
+
+ {% trans "Plan" %} +
+ {{ userplan.plan }} + + {% trans "Upgrade" %} + +
+
@@ -41,21 +57,15 @@
-
-
-
{% trans "Plan Details" %}
+
+
+
{% trans "Plan Details" %}
-
-
- {% include "plans/plan_table.html" %} -
-
+ {% include "plans/plan_table.html" %}
- - - +
{% endblock %} \ No newline at end of file diff --git a/templates/plans/invoices/layout.html b/templates/plans/invoices/layout.html index f17fe96a..0e370d08 100644 --- a/templates/plans/invoices/layout.html +++ b/templates/plans/invoices/layout.html @@ -1,171 +1,204 @@ +
+ +
{% if logo_url %} - company logo + Company Logo {% endif %} - -
-

- {{ invoice.full_number }} +
+
+

+ + {% 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 %} + +
{{ invoice.full_number }}

-

{% if not copy %}ORIGINAL{% else %}COPY{% endif %}

-

{{ invoice.issued|date:"Y-m-d" }}

- {% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %} -

{{ invoice.selling_date|date:"Y-m-d" }}

- {% else %} -

 

- {% endif %} +
+ {{ copy|yesno:"COPY,ORIGINAL" }} +
+
+
Issued: {{ invoice.issued|date:"F j, Y" }}
+ {% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %} +
Order Date: {{ invoice.selling_date|date:"F j, Y" }}
+ {% endif %} +
+
+
+

+ + +
+
+
+
+
Seller
+
+
+
+ {{ invoice.issuer_name }}
+ {{ invoice.issuer_street }}
+ {{ invoice.issuer_zipcode }} {{ invoice.issuer_city }}
+ {{ invoice.issuer_country.name }}
+ VAT ID: {{ invoice.issuer_tax_number }} +
+
+
- - - - - - - - + + + + + + + + + + + + + {% if invoice.rebate %} + + {% endif %} + + + + + + + + + + + + + + + +
- -

+
+
+
+
Buyer
+
+
+
+ {{ invoice.buyer_name }}
+ {{ invoice.buyer_street }}
+ {{ invoice.buyer_zipcode }} {{ invoice.buyer_city }}
+ {{ invoice.buyer_country.name }}
+ {% if invoice.buyer_tax_number %} + VAT ID: {{ invoice.buyer_tax_number }} + {% endif %} +
+
+
+
+ - {{ invoice.shipping_name }}
- {{ invoice.shipping_street }}
- {{ invoice.shipping_zipcode }} {{ invoice.shipping_city }}
- {{ invoice.buyer_country.code }} - {{ invoice.buyer_country.name }} + + {% if invoice.shipping_name %} +
+
+
Shipping Address
+
+
+
+ {{ invoice.shipping_name }}
+ {{ invoice.shipping_street }}
+ {{ invoice.shipping_zipcode }} {{ invoice.shipping_city }}
+ {{ invoice.buyer_country.name }} +
+
+
+ {% endif %} - -
- -

- {{ invoice.issuer_name }}
- {{ invoice.issuer_street }}
- {{ invoice.issuer_zipcode }} {{ invoice.issuer_city}}
- {{ invoice.issuer_country.code }} - {{ invoice.issuer_country.name }}

- {{ invoice.issuer_tax_number }}
-

- -

- {{ invoice.buyer_name }}
- {{ invoice.buyer_street }}
- {{ invoice.buyer_zipcode }} {{ invoice.buyer_city }}
- {{ invoice.buyer_country.code }} - {{ invoice.buyer_country.name }} - - {% if invoice.buyer_tax_number %} -

- - {{ invoice.buyer_tax_number }} - -

+ +
+
+
Invoice Items
+
+
+
+
+ + + + + + + + + {% if invoice.rebate %} + {% endif %} -
- - -
#DescriptionUnit PriceQty.UnitRebate
- - - - - - - - - - - {% if invoice.rebate %} - - - {% endif %} - - - - - - - - - - - - - - - {% if invoice.rebate %} - - {% endif %} - - - - - - - - - - - - - - - -
- - - - Description - - - - Unit price - - - - - Qty. - - - - Rebate - - - - Subtotal - - VAT - - - VAT Amount - - - - Subtotal with TAX/VAT -
- 1 - {{ invoice.item_description }}{{ invoice.unit_price_net|floatformat:2 }} {{ invoice.currency }}{{ invoice.quantity }}units{{ invoice.rebate|floatformat:2 }} %{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }} %{% else %}n/a{% endif %}{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}{{ invoice.total|floatformat:2 }} {{ invoice.currency }}
{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }} %{% else %}n/a{% endif %}{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}{{ invoice.total|floatformat:2 }} {{ invoice.currency }}
-
- - {% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %} -

- {% endif %} - - - - - {% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %} - - - {% else %} - - {% endif %} - - {{ invoice.payment_date|date:"Y-m-d" }} -

-
- - {% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}

This document is not an invoice.

{% endif %} - - {% if invoice.tax == None and invoice.is_UE_customer %} -

- -Reverse charge. -

- {% endif %} - - +
SubtotalVATVAT AmountTotal
1{{ invoice.item_description }}{{ invoice.unit_price_net|floatformat:2 }} {{ invoice.currency }}{{ invoice.quantity }}units{{ invoice.rebate|floatformat:2 }}%{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}%{% else %}n/a{% endif %}{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}{{ invoice.total|floatformat:2 }} {{ invoice.currency }}
Total{{ invoice.total_net|floatformat:2 }} {{ invoice.currency }}{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}%{% else %}n/a{% endif %}{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }} {{ invoice.currency }}{% else %}n/a{% endif %}{{ invoice.total|floatformat:2 }} {{ invoice.currency }}
+
+
+
+ + +
+
+
+
+
Payment Information
+
+
+ {% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %} +
+ Method: + Electronic Payment +
+ {% endif %} +
+ Due Date: + {{ invoice.payment_date|date:"F j, Y" }} +
+ {% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %} +
+ Payment Received +
+ {% endif %} +
+
+
+
+
+
+
Notes
+
+
+ {% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %} +
+ This document is not an invoice. +
+ {% endif %} + {% if invoice.tax == None and invoice.is_UE_customer %} +
+ Reverse charge applied. +
+ {% endif %} +
+
+
+
+ + +
+

Thank you for your business!

+

If you have any questions about this invoice, please contact us.

+
+
+ + \ No newline at end of file diff --git a/templates/plans/plan_table.html b/templates/plans/plan_table.html index 6f9ad952..c6995e99 100644 --- a/templates/plans/plan_table.html +++ b/templates/plans/plan_table.html @@ -1,89 +1,121 @@ -{% load i18n%} -
- - - - +{% load i18n %} +
+
+ + + {% for plan in plan_list %} - + + {% endfor %} {% for quota_row in plan_table %} - - + - {% for plan_quota in quota_row.1 %} - - {% endfor %} - + {% else %} + + {% if plan_quota.value == None %} + {% trans 'No Limit' %} + {% else %} + {{ plan_quota.value }} + {% if quota_row.0.unit %} + {{ quota_row.0.unit }} + {% endif %} + {% endif %} + + {% endif %} + {% endif %} + + {% endfor %} + {% endfor %} - - - - + + + + + {% if user.is_authenticated %} - - - {% for plan in plan_list %} - - {% endfor %} - - {% endif %} - - + + {% for plan in plan_list %} - + {% endfor %} + + {% endif %} + + + + {% for plan in plan_list %} + {% endfor %}
- {% if plan.url %}{% endif %} - {{ plan.name }} - {% if plan == userplan.plan %} - {% trans "Current Plan" %} - {% endif %} + +
- {% if quota_row.0.url %}{% endif %} - {{ quota_row.0.name }} - {{ quota_row.0.description }} +
+ - {% if plan_quota != None %} - {% if quota_row.0.is_boolean %} - {% if plan_quota.value %}{% else %}{% endif %} - {% else %} - {% if plan_quota.value == None %}{% trans 'No Limit' %}{% else %}{{ plan_quota.value }} {{ quota_row.0.unit }}{% endif %} - {% endif %} + + + {% for plan_quota in quota_row.1 %} + + {% if plan_quota != None %} + {% if quota_row.0.is_boolean %} + {% if plan_quota.value %} + + {% else %} + {% endif %} -
{% trans 'Pricing' %}
+
{% trans 'Pricing' %}
+
- {% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %} - {% trans "Change" %} - {% endif %} -
- {% if plan.available %} - + {% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %} + + {% trans "Change" %} + + {% endif %} +
+ {% if plan.available %} +
+ {% if not plan.is_free %} + {% for plan_pricing in plan.planpricing_set.all %} + {% if plan_pricing.visible %} +
+ {% if plan_pricing.pricing.url %}{% endif %} +
+ {{ plan_pricing.pricing.name }} + {{ plan_pricing.pricing.period }} {% trans "days" %} +
+ {% if plan_pricing.pricing.url %}
{% endif %} +
+ {{ plan_pricing.price }} {{ CURRENCY }} {% if plan_pricing.plan == userplan.plan or userplan.is_expired or userplan.plan.is_free %} - {% trans "Buy" %} + + {% trans "Buy" %} + {% endif %} - - {% endif %} - {% endfor %} - {% else %} -
  • - {% trans "Free" %} - ({% trans "no expiry" %}) - 0 {{ CURRENCY }} +
  • +
    + {% endif %} + {% endfor %} + {% else %} +
    +
    + {% trans "Free" %} + {% trans "no expiry" %} +
    +
    + 0 {{ CURRENCY }} {% if plan != userplan.plan or userplan.is_expired %} - + {% if userplan.is_expired %} {% trans "Select" %} {% else %} @@ -91,21 +123,22 @@ {% endif %} {% endif %} - - {% endif %} - - {% else %} - - {% url 'upgrade_plan' as upgrade_url %} - {% blocktrans %} - This plan is not available anymore and cannot be extended.
    - You need to upgrade your account to any of currently available plans. - {% endblocktrans %} -
    - {% endif %} - +
    +
    + {% endif %} +
    + {% else %} +
    + {% url 'upgrade_plan' as upgrade_url %} + {% blocktrans %} +

    This plan is no longer available.

    + Upgrade to current plans → + {% endblocktrans %} +
    + {% endif %} +
    -
    +
    \ No newline at end of file diff --git a/templates/pricing_page.html b/templates/pricing_page.html new file mode 100644 index 00000000..5d97b860 --- /dev/null +++ b/templates/pricing_page.html @@ -0,0 +1,352 @@ +{% extends 'base.html' %} +{% load i18n static %} +{% load custom_filters %} + +{% block customCSS %} + + + +{% endblock customCSS %} + +{% block content %} +
    +

    Choose Your Plan

    +
    + {% csrf_token %} + + +
    +

    1. Select a Plan

    +
    + {% for pp in plan_list %} +
    + + +
    + {% endfor %} +
    +
    + + +
    +

    2. Enter Your Information

    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    +
    + + +
    +

    3. Payment Information

    +
    +
    + +
    + + +
    +
    + +
    + +
    + + +
    +
    + +
    + +
    + + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + + +
    +

    4. Confirm Your Information

    +
    +
    Order Summary
    +
    Plan:
    +
    Price:
    +
    Tax (15%): 0.00 {{ CURRENCY }}
    +
    +
    Total: 0.00 {{ CURRENCY }}
    + +
    User Information
    +
    Name:
    +
    Email:
    +
    Company:
    +
    Phone:
    + +
    Payment
    +
    Cardholder:
    +
    Card Number:
    +
    Expiry:
    +
    +
    + + +
    + + + +
    +
    +
    +{% endblock content %} + + {% block customJS %} + + + {% endblock customJS %} \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html index f4a0ede0..6b1f7c96 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -2,6 +2,14 @@ {% load custom_filters %} {% load i18n static %} +{% block extraCSS %} + +{% endblock extraCSS %} {% block content %} @@ -84,31 +92,36 @@

    {{ _("Pricing") }}

    -
    - {% for plan in plan_list %} -
    -
    -
    -
    -

    {{ plan.name }}

    -

    {{ plan.planpricing_set.first.price }} {{ CURRENCY }} / {{ _("Per month")}}

    +
    + {% for plan in plan_list %} +
    + -
    {{ _("Included")}}
    -
      - {% for line in plan.description|splitlines %} -
    • - - {{ line }} +
    +
    +
    +
    -
    + {% endfor %}
    - {% endfor %} -
    +