From f117feee6dbe973bafd5c9a4818c591b5a79afd2 Mon Sep 17 00:00:00 2001 From: ismail Date: Tue, 29 Jul 2025 13:18:00 +0300 Subject: [PATCH] fix the plans upgrade and more --- inventory/forms.py | 13 +- inventory/signals.py | 10 +- inventory/tasks.py | 5 +- inventory/urls.py | 5 + inventory/views.py | 175 +++++----- static/images/logos/users/user-logo.jpg | Bin 0 -> 2537 bytes templates/account/signup-wizard-nkp.html | 389 ----------------------- templates/account/signup-wizard.html | 6 +- templates/base.html | 1 + templates/components/note_modal.html | 23 +- templates/crm/leads/lead_detail.html | 12 +- templates/dealers/dealer_detail.html | 2 +- templates/pricing_page.html | 313 ++++++++++-------- templates/users/user_detail.html | 8 + templates/users/user_list.html | 9 +- templates/users/user_password_reset.html | 22 ++ 16 files changed, 356 insertions(+), 637 deletions(-) create mode 100644 static/images/logos/users/user-logo.jpg delete mode 100644 templates/account/signup-wizard-nkp.html create mode 100644 templates/users/user_password_reset.html diff --git a/inventory/forms.py b/inventory/forms.py index 57a396d1..65dc6595 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -16,7 +16,7 @@ from django_ledger.forms.invoice import ( ) from django.forms.models import inlineformset_factory from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase - +from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm from django_ledger.forms.journal_entry import ( JournalEntryModelCreateForm as JournalEntryModelCreateFormBase, ) @@ -2102,3 +2102,14 @@ class VatRateForm(forms.ModelForm): class Meta: model = VatRate fields = ["rate"] + + +class CustomSetPasswordForm(SetPasswordForm): + new_password1 = forms.CharField( + label="New Password", + widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'}) + ) + new_password2 = forms.CharField( + label="Confirm New Password", + widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'}) + ) \ No newline at end of file diff --git a/inventory/signals.py b/inventory/signals.py index ca596175..1a0bc707 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from decimal import Decimal from django.urls import reverse from inventory.tasks import create_coa_accounts, create_make_accounts @@ -24,6 +25,7 @@ from . import models from django.utils.timezone import now from django.db import transaction from django_q.tasks import async_task +from plans.models import UserPlan from plans.signals import order_completed, activate_user_plan # logging @@ -1212,10 +1214,4 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs): kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk}, ), ), - ) - - -def handle_upgrade(sender, order, **kwargs): - logger.info(f"User {order.user} upgraded to {order.plan}") - -order_completed.connect(handle_upgrade) \ No newline at end of file + ) \ No newline at end of file diff --git a/inventory/tasks.py b/inventory/tasks.py index c285443e..9f319af9 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -1,4 +1,4 @@ - +import base64 import logging from plans.models import Plan from django.conf import settings @@ -8,10 +8,11 @@ from django_q.tasks import async_task from django.core.mail import send_mail from appointment.models import StaffMember from django.utils.translation import activate +from django.core.files.base import ContentFile from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress -from django.core.mail import EmailMultiAlternatives from inventory.models import DealerSettings, Dealer +from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User, Group, Permission diff --git a/inventory/urls.py b/inventory/urls.py index 093ffcd9..19a79ef8 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -517,6 +517,11 @@ urlpatterns = [ views.UserDeleteview, name="user_delete", ), + path( + "/user//password_reset/", + views.staff_password_reset_view, + name="staff_password_reset", + ), path( "/group/create/", views.GroupCreateView.as_view(), diff --git a/inventory/views.py b/inventory/views.py index 502ccb9c..6e488c7a 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,4 +1,6 @@ # Standard +from django.core.files.base import ContentFile +import base64 import os import re import io @@ -15,7 +17,7 @@ from random import randint from decimal import Decimal from io import TextIOWrapper from django.apps import apps -from datetime import datetime +from datetime import datetime, timedelta from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -301,18 +303,20 @@ def dealer_signup(request): :rtype: Union[django.http.HttpResponse, django.http.JsonResponse] """ if request.method == "POST": - data = json.loads(request.body) - - email = data.get("email") - password = data.get("password") - password_confirm = data.get("confirm_password") - name = data.get("name") - arabic_name = data.get("arabic_name") - phone = data.get("phone_number") - crn = data.get("crn") - vrn = data.get("vrn") - address = data.get("address") + try: + data = json.loads(request.body) + email = data.get("email") + password = data.get("password") + password_confirm = data.get("confirm_password") + name = data.get("name") + arabic_name = data.get("arabic_name") + phone = data.get("phone_number") + crn = data.get("crn") + vrn = data.get("vrn") + address = data.get("address") + except json.JSONDecodeError: + pass if User.objects.filter(email=email).exists(): return JsonResponse({"error": _("Email already exists")}, status=400) if not re.match( @@ -418,11 +422,11 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) dealer = get_user_type(self.request) entity = dealer.entity - total_cars = models.Car.objects.filter(dealer=dealer).count() - total_reservations = models.CarReservation.objects.filter( - reserved_until__gte=timezone.now() - ).count() - stats = models.CarFinance.objects.aggregate( + + qs = models.Car.objects.filter(dealer=dealer) + total_cars = qs.count() + + stats = models.CarFinance.objects.filter(car__dealer=dealer).aggregate( total_cost_price=Sum("cost_price"), total_selling_price=Sum("selling_price"), ) @@ -439,72 +443,48 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView): canceled_leads = models.Lead.objects.filter( dealer=dealer, status=models.Status.UNQUALIFIED ).count() - available_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.AVAILABLE - ).count() - sold_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.SOLD - ).count() - reserved_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.RESERVED - ).count() - hold_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.HOLD - ).count() - damaged_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.DAMAGED - ).count() - transfer_cars = models.Car.objects.filter( - dealer=dealer, status=models.CarStatusChoices.TRANSFER - ).count() - try: - reserved_percentage = reserved_cars / total_cars * 100 - except ZeroDivisionError as e: - print(f"error: {e}") - try: - sold_percentage = sold_cars / total_cars * 100 - except ZeroDivisionError as e: - print(f"error: {e}") - qs = ( - models.Car.objects.values("id_car_make__name") - .annotate(count=Count("id")) - .order_by("id_car_make__name") - ) - car_by_make = list(qs) - total_activity = models.UserActivityLog.objects.filter(user=dealer.user).count() - staff = models.Staff.objects.filter(dealer=dealer).count() - total_leads = models.Lead.objects.filter(dealer=dealer).count() - invoices = entity.get_invoices().count() - customers = entity.get_customers().count() - purchase_orders = entity.get_purchase_orders().count() - estimates = entity.get_estimates().count() + car_status_qs = qs.values("status").annotate(count=Count("status")) + car_status = {status: count for status, count in car_status_qs} + + car_by_make_qs = qs.values("id_car_make__name").annotate(count=Count("id")) + car_by_make = list(car_by_make_qs) context["dealer"] = dealer - context["total_activity"] = total_activity + context["total_activity"] = models.UserActivityLog.objects.filter( + user=dealer.user + ).count() context["total_cars"] = total_cars - context["total_reservations"] = total_reservations + context["total_reservations"] = models.CarReservation.objects.filter( + reserved_until__gte=timezone.now() + ).count() context["total_cost_price"] = total_cost_price context["total_selling_price"] = total_selling_price context["total_profit"] = total_profit context["new_leads"] = new_leads context["pending_leads"] = pending_leads context["canceled_leads"] = canceled_leads - context["reserved_percentage"] = reserved_percentage - context["sold_percentage"] = sold_percentage - context["available_cars"] = available_cars - context["sold_cars"] = sold_cars - context["reserved_cars"] = reserved_cars - context["hold_cars"] = hold_cars - context["damaged_cars"] = damaged_cars - context["transfer_cars"] = transfer_cars context["car"] = json.dumps(car_by_make) - context["customers"] = customers - context["staff"] = staff - context["total_leads"] = total_leads - context["invoices"] = invoices - context["estimates"] = estimates - context["purchase_orders"] = purchase_orders + context["available_cars"] = car_status.get( + models.CarStatusChoices.AVAILABLE, 0 + ) + context["sold_cars"] = car_status.get(models.CarStatusChoices.SOLD, 0) + context["reserved_cars"] = car_status.get( + models.CarStatusChoices.RESERVED, 0 + ) + context["hold_cars"] = car_status.get(models.CarStatusChoices.HOLD, 0) + context["damaged_cars"] = car_status.get( + models.CarStatusChoices.DAMAGED, 0 + ) + context["transfer_cars"] = car_status.get( + models.CarStatusChoices.TRANSFER, 0 + ) + context["customers"] = entity.get_customers().count() + context["staff"] = models.Staff.objects.filter(dealer=dealer).count() + context["total_leads"] = models.Lead.objects.filter(dealer=dealer).count() + context["invoices"] = entity.get_invoices().count() + context["estimates"] = entity.get_estimates().count() + context["purchase_orders"] = entity.get_purchase_orders().count() return context @@ -3416,6 +3396,7 @@ class UserCreateView( success_url = reverse_lazy("user_list") success_message = _("User created successfully") permission_required = ["inventory.add_staff"] + staff_pk = None def get_form(self, form_class=None): form = super().get_form(form_class) @@ -3425,13 +3406,6 @@ class UserCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - # 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 dealer.is_staff_exceed_quota_limit: messages.error( self.request, @@ -3463,12 +3437,14 @@ class UserCreateView( staff.staff_member = staff_member staff.dealer = dealer staff.save() + self.staff_pk = staff.pk for customgroup in form.cleaned_data["group"]: staff.add_group(customgroup.group) return super().form_valid(form) def get_success_url(self): - return reverse_lazy("user_list", args=[self.request.dealer.slug]) + return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) + # return reverse_lazy("user_list", args=[self.request.dealer.slug]) class UserUpdateView( @@ -6148,6 +6124,7 @@ def lead_tracking(request, dealer_slug): @permission_required("inventory.change_lead", raise_exception=True) def update_lead_actions(request, dealer_slug): # get the user info for logging + user_username = ( request.user.username if request.user.is_authenticated else "anonymous" ) @@ -9391,9 +9368,7 @@ def payment_callback(request, dealer_slug): history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() payment_status = request.GET.get("status") order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW - if payment_status == "paid": - # Get or create billing info (optional step) billing_info, created = BillingInfo.objects.get_or_create( user=dealer.user, defaults={ @@ -9407,14 +9382,20 @@ def payment_callback(request, dealer_slug): ) try: - # COMPLETE THE ORDER - This handles plan activation/upgrade - order.complete_order() # Critical step: activates the plan + order.complete_order() + user = order.user + pricing = order.get_plan_pricing().pricing + + logger.info(f"Processing order completion for {user} - upgrading to {order.plan}") + user.userplan.plan = order.plan + user.userplan.expire = datetime.now() + timedelta(days=pricing.period) + user.userplan.save() + user.save() + logger.info(f"User {user} upgraded to {order.plan} plan successfully") - # Update payment history history.status = "paid" history.save() - # Retrieve invoice invoice = order.get_invoices().first() return render( request, @@ -9423,7 +9404,6 @@ def payment_callback(request, dealer_slug): ) except Exception as e: - # Handle activation errors (log, notify admin, etc.) logger.error(f"Plan activation failed: {str(e)}") history.status = "failed" history.save() @@ -9434,7 +9414,6 @@ def payment_callback(request, dealer_slug): history.save() return render(request, "payment_failed.html", {"message": message}) - # Handle unexpected status return render(request, "payment_failed.html", {"message": "Unknown payment status"}) # def payment_callback(request, dealer_slug): # message = request.GET.get("message") @@ -9685,6 +9664,7 @@ def update_schedule(request, dealer_slug, pk): @permission_required("inventory.add_notes", raise_exception=True) def add_note(request, dealer_slug, content_type, slug): # Get user information for logging + print("hi") user_username = ( request.user.username if request.user.is_authenticated else "anonymous" ) @@ -9743,6 +9723,7 @@ def add_note(request, dealer_slug, content_type, slug): @permission_required("inventory.change_notes", raise_exception=True) def update_note(request, dealer_slug, pk): note = get_object_or_404(models.Notes, pk=pk) + print(note) lead = get_object_or_404(models.Lead, pk=note.content_object.id) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) if request.method == "POST": @@ -10750,3 +10731,21 @@ def car_sale_report_csv_export(request,dealer_slug): ]) return response + + +@login_required +# @permission_required('inventory.view_staff') +def staff_password_reset_view(request, dealer_slug, user_pk): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first() + + if request.method == 'POST': + form = forms.CustomSetPasswordForm(staff.user, request.POST) + if form.is_valid(): + form.save() + messages.success(request, _('Your password has been set. You may go ahead and log in now.')) + return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug) + else: + messages.error(request, _('Invalid password. Please try again.')) + form = forms.CustomSetPasswordForm(staff.user) + return render(request, 'users/user_password_reset.html', {'form': form}) \ No newline at end of file diff --git a/static/images/logos/users/user-logo.jpg b/static/images/logos/users/user-logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bb7e4923ad63647e5206efb56724f5215c9fd237 GIT binary patch literal 2537 zcmb7`c`)1E9>#x3M1&$y^cLksR2MS#YEw%Pltycb+N+kgwToR#RV}T3d(|2((O2x1 zB4VkfUQ6w5Gz7KPzE_o2%jMp=^WK^J*ZrJlzH{b$&-2Wg^T(NUIC3}zAPjV`=>i}S z2+%(oz~LC64M4!h99PH@Z0u~uaImvO+2I_VoNx{}obv?l2~I8^E;yVU$<4!yKp+vE zC-_eCAx<965yv5*V<&`-BV9C_4`{;x{{85^K%2ttQL4R9>IyxZ|FyH8VuOxX%dL|~xYa4~1 z#0e@r%yqqFL-Q0?#h(EMDL1b>u#PJBCsCs3E%DTx({)Mut5jBWWA zs;(-LhYieO3Wbqj&i`<22$reK3)*46O8v1pAZyjKv3|==ZlqHDMYqkJ40>4lH$#H+ zry(W3nzoZ__9Y2TFELFHCQCsE9o{^^vTm+9%$%GIDB2I9y$z8nrp_U6J1$qfegoVM7GjR~iE~>3fw|p=nuOclTm>kof>IF}hm{ z2UU5&?dUWhJ2$na)+14Z!IwmLdqp!n9%29)GoOCq|p>=g9-34BaurvAAl~_pymVzp|zRVI5n&h(?)e7L)t# zLp0sK2bWqq`0jp~bwQ|8nZhmkiualp6WUx@H(DTx!q^7I^tcF1ig)jZ-Ue(auhe9VTscAfQ z@QE|NJ*`lM6r|@$Na&zbd-##+E`{#Ij`s^q>!5N-3@Q&BXwl<=INU2g5f~-8$RR*(6k97v(m;GXLD@(@$k(P!M)*GF79v<6-7XhE;9kFW3A) zt9ZJ!^gqleeS1kiRocti1haJF8)7r})x=IG)MvQ$Bn4_wjSwAsl^~n3z_v@QQfJNI z>U1`8{s_Ew`kUQV5l*T%sp+eE|3_Qx<)>+WU%hUOC4IGHs*B$vK)rR=4H2V!*e|>o z7sUwPzLjL0-oskY+BeyXtEw2=Q8F`ewp!9isu0;3e%bim+3wwvI3s_Si$i)}?8*&O zoFaB=di1n#Wg6LavIhG==Voct$aaQc4c;mk( zV@ZAO)Mu_F6EgEQ;fdWrZ-_wFu==FNH5euIlL@MdP&O{xac|^0gVj{eBh_pX_S}cM zcu|^W{?-;xNj>$1_C$kMLiGV-=srn$qTpw^|9AB#>)sD@9&(8VC%2}1>oMOun&J%@ ztOV0^dn}tLuLKI`7=q*W7m(tHbSLH~6lWMkEWg^{k?y(}(CX&@ex}Rr!>Hnd)bx(t zjo_(2Z-4$FMsNM$v4_TSpA0t9x=I#<)^Aunc-JJ0+y0tv1*3jE=pQLhC=I|$qkK`M zfmuikoPS5*ev{H`&qc|E{<$8_H-a%|)Jjqv;OMBx&6Og7i}SI`n@X<^GWUKfAVaW* z9EU)Bh+y-5-@TAuQq4E_8=IU0+<#$;Ul+l;l!eA0X!Hq>9hk)@YIlc>8D8&%DdfrN lA+!PUP&47uL~3hQ%!N-$nlF8io?{`e -
-
-
- -
- {% trans 'home' %} - {% trans 'home' %} -
-
-
-

{% trans 'Sign Up' %}

-

{% trans 'Create your account today' %}

-
-
- -
-
-
-
-
- - -
{% trans "Please enter a valid email address" %}
-
-
- - -
- {% trans "Password does not match. or length is less than 8 characters." %} -
-
-
- - * - -
- {% trans "Password does not match. or length is less than 8 characters." %} -
-
-
-
-
-
-
- - -
-
- - -
-
- - * - -
{% trans "Please enter a valid phone number" %}
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
- - -
-
-
-
-
{% trans 'You are all set!' %}
-

- {% trans 'Now you can access your account' %} -
- {% trans 'anytime' %} {% trans 'anywhere' %} -

- -
-
-
-
-
-
- -
-
-
-
- -
- {% include 'footer.html' %} -
- -{% endblock content %} -{% block customJS %} - - - - -{% endblock customJS %} diff --git a/templates/account/signup-wizard.html b/templates/account/signup-wizard.html index a23d1d16..52dac1a7 100644 --- a/templates/account/signup-wizard.html +++ b/templates/account/signup-wizard.html @@ -282,9 +282,9 @@ - +
{% include 'footer.html' %} - +
{% endblock content %} {% block customJS %} @@ -307,7 +307,7 @@ return true } function validate_sa_phone_number(phone_number) { - const phone_numberRegex = /^05[0-9]{8}$/; + const phone_numberRegex = /^056[0-9]{7}$/; return phone_numberRegex.test(phone_number) && phone_numberRegex !== ''; } function getAllFormData() { diff --git a/templates/base.html b/templates/base.html index 7f6b4995..3e2a4fb9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -70,6 +70,7 @@ + {% comment %} {% endcomment %} {% comment %} {% block customCSS %}{% endblock %} {% endcomment %} diff --git a/templates/components/note_modal.html b/templates/components/note_modal.html index 25e3002f..0081f8eb 100644 --- a/templates/components/note_modal.html +++ b/templates/components/note_modal.html @@ -15,7 +15,7 @@ @@ -47,7 +47,7 @@
- +
@@ -800,9 +800,9 @@ - +
@@ -824,7 +824,7 @@ let form = document.querySelector('.add_note_form') form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" } - let Toast = Swal.mixin({ + /*let Toast = Swal.mixin({ toast: true, position: "top-end", showConfirmButton: false, @@ -834,7 +834,7 @@ toast.onmouseenter = Swal.stopTimer; toast.onmouseleave = Swal.resumeTimer; } - }); + });*/ // Display Django messages {% if messages %} diff --git a/templates/dealers/dealer_detail.html b/templates/dealers/dealer_detail.html index ae726804..59bab6df 100644 --- a/templates/dealers/dealer_detail.html +++ b/templates/dealers/dealer_detail.html @@ -121,7 +121,7 @@ background-position:left bottom; background-size:auto">
-
+

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

diff --git a/templates/pricing_page.html b/templates/pricing_page.html index 20d256aa..2e486d35 100644 --- a/templates/pricing_page.html +++ b/templates/pricing_page.html @@ -310,162 +310,219 @@ {% endblock content %} {% block customJS %} {% endblock customJS %} diff --git a/templates/users/user_detail.html b/templates/users/user_detail.html index fddd47f7..46663f81 100644 --- a/templates/users/user_detail.html +++ b/templates/users/user_detail.html @@ -36,6 +36,9 @@

{{ _("Arabic Name") }}: {{ user_.arabic_name }}

+

+ {{ _("Email") }}: {{ user_.email }} +

@@ -94,6 +97,11 @@ {{ _("Back to List") }} + + {{ _("Reset Password") }} + +

diff --git a/templates/users/user_list.html b/templates/users/user_list.html index 0da15b7d..f426b51c 100644 --- a/templates/users/user_list.html +++ b/templates/users/user_list.html @@ -6,7 +6,6 @@ {% endblock title %} {% block content %} - {%if users %}
@@ -23,7 +22,7 @@ role="alert">

- {{ _("No Active Subscription,please activate your subscription.") }}Manage Subscription

+ +
+
+
+
+
+{% endblock %} \ No newline at end of file