diff --git a/inventory/forms.py b/inventory/forms.py index 7089573a..3286cee3 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -1288,6 +1288,32 @@ class OpportunityForm(forms.ModelForm): if self.instance and self.instance.pk: self.fields["probability"].initial = self.instance.probability +class OpportunityStageForm(forms.ModelForm): + """ + Represents a form for creating or editing Opportunity instances. + + This class is a Django ModelForm designed to simplify the process of + validating and persisting data for Opportunity model instances. It + maps fields from the Opportunity model to form fields, making it + convenient to handle user input for operations such as creating and + updating opportunities. + + :ivar Meta.model: The model associated with the form. + :type Meta.model: type + :ivar Meta.fields: List of fields from the model included in the form. + :type Meta.fields: list + """ + + + class Meta: + model = Opportunity + fields = [ + "stage", + + ] + + + class InvoiceModelCreateForm(InvoiceModelCreateFormBase): """ diff --git a/inventory/urls.py b/inventory/urls.py index 2a8db5cd..c92f9901 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -228,6 +228,11 @@ urlpatterns = [ views.OpportunityUpdateView.as_view(), name="update_opportunity", ), + path( + "/crm/opportunities//stage/edit", + views.OpportunityStageUpdateView.as_view(), + name="update_opportunity_stage", + ), path( "/crm/opportunities/", views.OpportunityListView.as_view(), @@ -928,6 +933,7 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), + # Expanese path( "/items/expeneses/", @@ -1284,6 +1290,10 @@ urlpatterns = [ path('/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), + # staff profile + path('/staff/detail/', views.StaffDetailView.as_view(), name='staff_detail'), + + ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/views.py b/inventory/views.py index ecd622f7..f87b5c89 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -2197,6 +2197,35 @@ class DealerUpdateView( def get_success_url(self): return reverse("dealer_detail", kwargs={"slug": self.object.slug}) + +class StaffDetailView(LoginRequiredMixin, DetailView): + """ + Represents a detailed view for a Dealer model. + + This class extends Django's `DetailView` to provide a detailed view of a dealer. + It includes additional context data such as the count of staff members, cars + associated with the dealer, available car makes, and dynamically fetched quotas. + The class also ensures that users must be logged in to access the detailed view. + + :ivar model: The model associated with this view (Dealer model). + :type model: django.db.models.Model + :ivar template_name: Path to the template used to render the view. + :type template_name: str + :ivar context_object_name: The name used to refer to the object in the template context. + :type context_object_name: str + """ + + model = models.Staff + template_name = "staff/staff_detail.html" + context_object_name = "staff" + + + +def dealer_vat_rate_update(request, slug): + dealer = get_object_or_404(models.Dealer, slug=slug) + models.VatRate.objects.filter(dealer=dealer).update(rate=request.POST.get("rate")) + messages.success(request, _("VAT rate updated successfully")) + return redirect("dealer_detail", slug=slug) class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): @@ -6791,27 +6820,37 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # return response # return redirect("lead_list", dealer_slug=dealer_slug) msg = f""" - السلام عليكم - Dear {lead.full_name}, +السلام عليكم {lead.full_name}, - أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة. +شكراً لزيارتك لـ {lead.dealer.name}! لقد كان من دواعي سرورنا مساعدتك اليوم. - I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached. +لقد أنشأنا ملفاً شخصياً لك في نظامنا لتتبع تفضيلاتك والسيارات التي تهتم بها. سنتواصل معك قريباً للمتابعة والإجابة على أي أسئلة أخرى قد تكون لديك. - يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع. +في هذه الأثناء، لا تتردد في الاتصال بنا مباشرة على {lead.dealer.phone_number} أو زيارتنا مرة أخرى في أي وقت يناسبك. - Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project. +نتطلع إلى مساعدتك في العثور على سيارتك القادمة! - شكراً لاهتمامكم بهذا الأمر. - Thank you for your attention to this matter. +تحياتي، +{lead.dealer.arabic_name} +{lead.dealer.address} +{lead.dealer.phone_number} +----- +Dear {lead.full_name}, - تحياتي, - Best regards, - [Your Name] - [Your Position] - [Your Company] - [Your Contact Information] - """ +Thank you for visiting {lead.dealer.name}! It was a pleasure to assist you today. + +We've created a profile for you in our system to keep track of your preferences and the vehicles you're interested in. We'll be in touch shortly to follow up and answer any further questions you may have. + +In the meantime, feel free to contact us directly at {lead.dealer.phone_number} or visit us again at your convenience. + +We look forward to helping you find your next car! + +Best regards, +{lead.dealer.name} +{lead.dealer.address} +{lead.dealer.phone_number} + +""" subject = "" if email_pk: email = get_object_or_404(models.Email, pk=email_pk) @@ -6851,7 +6890,7 @@ class OpportunityCreateView( template_name = "crm/opportunities/opportunity_form.html" success_message = _("Opportunity created successfully.") permission_required = ["inventory.add_opportunity"] - + def get_initial(self): initial = super().get_initial() dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) @@ -6927,6 +6966,7 @@ class OpportunityUpdateView( template_name = "crm/opportunities/opportunity_form.html" success_message = _("Opportunity updated successfully.") permission_required = ["inventory.change_opportunity"] + def get_form(self, form_class=None): form = super().get_form(form_class) @@ -6949,6 +6989,46 @@ class OpportunityUpdateView( }, ) +class OpportunityStageUpdateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + """ + Handles the update functionality for Opportunity objects. + + This class-based view is responsible for handling the update of existing + Opportunity instances. It uses a Django form that is specified by the + `form_class` attribute and renders a template to display and process the + update form. Access to this view is restricted to authenticated users, as + it inherits from `LoginRequiredMixin`. + + It defines the model to be updated and the form template to be used. Upon + successful update, it redirects the user to the detail page of the updated + opportunity instance. + + :ivar model: The model associated with this view. Represents the Opportunity model. + :type model: django.db.models.Model + :ivar form_class: The form class used to manage the Opportunity update process. + :type form_class: django.forms.ModelForm + :ivar template_name: The path to the template used to render the opportunity + update form. + :type template_name: str + """ + + model = models.Opportunity + form_class = forms.OpportunityStageForm + success_message = _("Opportunity Stage updated successfully.") + permission_required = ["inventory.change_opportunity"] + + + def get_success_url(self): + return reverse_lazy( + "opportunity_detail", + kwargs={ + "dealer_slug": self.kwargs.get("dealer_slug"), + "slug": self.object.slug, + }, + ) + class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ @@ -6971,7 +7051,7 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV template_name = "crm/opportunities/opportunity_detail.html" context_object_name = "opportunity" permission_required = ["inventory.view_opportunity"] - + def get_context_data(self, **kwargs): dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) context = super().get_context_data(**kwargs) @@ -7043,6 +7123,7 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV "schedules": qs.filter(scheduled_at__gt=timezone.now())[:5], } context["schedule_form"] = forms.ScheduleForm() + context["stage_form"] = forms.OpportunityStageForm() return context @@ -7063,17 +7144,11 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) staff = self.request.staff queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) - # Search filter - search = self.request.GET.get("q") - if search: - queryset = queryset.filter( - Q(customer__first_name__icontains=search) - | Q(customer__last_name__icontains=search) - | Q(customer__email__icontains=search) - ) + # Stage filter stage = self.request.GET.get("stage") + print(stage) if stage: queryset = queryset.filter(stage=stage) @@ -7084,7 +7159,16 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) elif sort == "highest": queryset = queryset.order_by("-expected_revenue") elif sort == "closing": - queryset = queryset.order_by("closing_date") + queryset = queryset.order_by("expected_close_date") + + # Search filter + search = self.request.GET.get("q") + if search: + queryset = queryset.filter( + Q(customer__first_name__icontains=search) + | Q(customer__last_name__icontains=search) + | Q(customer__email__icontains=search) + ) return queryset @@ -7332,10 +7416,15 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) query = self.request.GET.get("q") qs = models.AdditionalServices.objects.filter(dealer=dealer).all() if query: - qs = apply_search_filters(qs, query) + qs = qs.filter(Q(name__icontains=query)| + Q(id__icontains=query)| + Q(uom__icontains=query) + ) return qs + + class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ Represents a view for creating item expense entries. @@ -7427,6 +7516,9 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV ) + + + class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -7828,27 +7920,44 @@ def send_email_view(request, dealer_slug, pk): ) msg = f""" - السلام عليكم - Dear {estimate.customer.customer_name}, +السلام عليكم، - أود أن أشارككم عرض السعر. +عزيزي {estimate.customer.customer_name}، - I wanted to share with you the quotation. +يسعدني أن أشارككم عرض السعر الذي طلبتموه. يرجى الاطلاع على التفاصيل الكاملة والأسعار من خلال الرابط أدناه. - يرجى مراجعة عرض السعر وإعلامي إذا كانت لديك أي استفسارات أو ملاحظات. إذا كان كل شيء على ما يرام، يمكننا المتابعة في الإجراءات. +حرصنا على أن يكون عرضنا مناسباً وشفافاً. إذا كانت لديكم أي استفسارات أو ملاحظات، فلا تترددوا في التواصل معنا. - Please review the quotation and let me know if you have any questions or concerns. If everything looks good, we can proceed with the process. +رابط عرض السعر: +{link} - رابط عرض السعر: - {link} +نأمل أن ينال العرض إعجابكم ونتطلع إلى بدء العمل قريباً! +تحياتي، - تحياتي, - Best regards, - {dealer.get_local_name} - {dealer.phone_number} - هيكل | Haikal - """ +{dealer.get_local_name} +{dealer.phone_number} +Haikal | هيكل +----- +Dear {estimate.customer.customer_name}, + +I hope this email finds you well. + +Following up on our conversation, I'm excited to share the quotation for your review. Please find the detailed pricing and information by clicking on the link below. + +We've done our best to provide you with a fair and competitive offer. If you have any questions or would like to discuss it further, please don't hesitate to reach out. + +Quotation Link: +{link} + +We look forward to hearing from you and hopefully moving forward with your project! + +Best regards, + +{dealer.get_local_name} +{dealer.phone_number} +Haikal +""" # subject = _("Quotation") send_email( @@ -9387,10 +9496,12 @@ def submit_plan(request, dealer_slug): tax=15, status=1, ) + logger.info(f"order created {order}") except Exception as e: - print(e) + logger.error(e) if not order: messages.error(request, _("Error creating order")) + logger.error("unable to create order") return redirect("pricing_page", dealer_slug=dealer_slug) transaction_url = handle_payment(request, order) return redirect(transaction_url) @@ -9404,9 +9515,13 @@ def payment_callback(request, dealer_slug): payment_id = request.GET.get("id") history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() payment_status = request.GET.get("status") + logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW + print(order) if payment_status == "paid": + logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.") + billing_info, created = BillingInfo.objects.get_or_create( user=dealer.user, defaults={ @@ -9418,12 +9533,20 @@ def payment_callback(request, dealer_slug): 'country': dealer.entity.country or " ", } ) + if created: + logger.info(f"Created new billing info for user {dealer.user}.") + else: + logger.debug(f"Billing info already exists for user {dealer.user}.") + if not hasattr(order.user, 'userplan'): UserPlan.objects.create( user=order.user, plan=order.plan, expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) ) + logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") + else: + logger.info(f"UserPlan already exists for user {order.user}.") try: @@ -9441,7 +9564,7 @@ def payment_callback(request, dealer_slug): order.complete_order() history.status = "paid" history.save() - + logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.") invoice = order.get_invoices().first() return render( request, @@ -9450,12 +9573,14 @@ def payment_callback(request, dealer_slug): ) except Exception as e: + logger.exception(f"Error completing order {order.id} for user {order.user}: {e}") logger.error(f"Plan activation failed: {str(e)}") history.status = "failed" history.save() return render(request, "payment_failed.html", {"message": "Plan activation error"}) elif payment_status == "failed": + logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") history.status = "failed" history.save() return render(request, "payment_failed.html", {"message": message}) @@ -10271,8 +10396,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie def get_context_data(self, **kwargs): dealer = get_user_type(self.request) + vendors=models.Vendor.objects.filter(dealer=dealer) context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug + context["vendors"] = vendors + return context @@ -10716,13 +10844,69 @@ def purchase_report_csv_export(request,dealer_slug): ]) return response +# @login_required +# def car_sale_report_view(request,dealer_slug): +# dealer = get_object_or_404(models.Dealer, slug=dealer_slug) +# cars_sold = models.Car.objects.filter(dealer=dealer,status='sold') +# current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") +# context={'cars_sold':cars_sold,'current_time':current_time } +# return render(request,'ledger/reports/car_sale_report.html',context) + + @login_required -def car_sale_report_view(request,dealer_slug): - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - cars_sold = models.Car.objects.filter(dealer=dealer,status='sold') - current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") - context={'cars_sold':cars_sold,'current_time':current_time } - return render(request,'ledger/reports/car_sale_report.html',context) +def car_sale_report_view(request, dealer_slug): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + + # Get filter parameters from the request + selected_make = request.GET.get('make') + selected_model = request.GET.get('model') + selected_serie = request.GET.get('serie') + selected_year = request.GET.get('year') + + # Apply filters to the queryset + if selected_make: + cars_sold = cars_sold.filter(id_car_make__name=selected_make) + if selected_model: + cars_sold = cars_sold.filter(id_car_model__name=selected_model) + if selected_serie: + cars_sold = cars_sold.filter(id_car_serie__name=selected_serie) + if selected_year: + cars_sold = cars_sold.filter(year=selected_year) + + # Get distinct values for filter dropdowns + makes = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_make__name', flat=True).distinct() + models_qs = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_model__name', flat=True).distinct() + series = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_serie__name', flat=True).distinct() + years = models.Car.objects.filter(dealer=dealer, status='sold').values_list('year', flat=True).distinct().order_by('-year') + + # # Calculate summary data for the filtered results + + total_revenue = cars_sold.aggregate(total_revenue=Sum('finances__marked_price'))['total_revenue'] or 0 + # total_vat = cars_sold.aggregate(total_vat=Sum('finances__vat_amount'))['total_vat'] or 0 + total_discount = cars_sold.aggregate(total_discount=Sum('finances__discount_amount'))['total_discount'] or 0 + + current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") + + context = { + 'cars_sold': cars_sold, + 'current_time': current_time, + 'dealer': dealer, + 'total_revenue': total_revenue, + # 'total_vat': total_vat, + 'total_discount': total_discount, + 'makes': makes, + 'models': models_qs, + 'series': series, + 'years': years, + 'selected_make': selected_make, + 'selected_model': selected_model, + 'selected_serie': selected_serie, + 'selected_year': selected_year, + } + + return render(request, 'ledger/reports/car_sale_report.html', context) + def car_sale_report_csv_export(request,dealer_slug): @@ -10766,10 +10950,10 @@ def car_sale_report_csv_export(request,dealer_slug): car.year, car.id_car_serie.name, car.id_car_trim.name, - car.mileage, + car.mileage if car.mileage else '0', car.stock_type, car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', - car.sold_date.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', + car.sold_date.strftime("%Y-%m-%d %H:%M:%S") if car.sold_date else '', car.finances.cost_price, car.finances.marked_price, car.finances.discount_amount, diff --git a/static/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg b/static/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg new file mode 100644 index 00000000..4b267969 Binary files /dev/null and b/static/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg differ diff --git a/staticfiles/css/calendar_dark.css b/staticfiles/css/calendar_dark.css new file mode 100644 index 00000000..bb5bc90f --- /dev/null +++ b/staticfiles/css/calendar_dark.css @@ -0,0 +1,105 @@ + +/* Card and container styling */ +.card { + background-color: #2d3748; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border: 1px solid #4a5568; +} + +/* FullCalendar header */ +.fc .fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; +} + +.fc .fc-toolbar-title { + font-size: 1.5rem; + font-weight: 600; + color: #edf2f7; +} + +/* Calendar buttons */ +.fc .fc-button-group > .fc-button { + background-color: #4a5568; + border-color: #4a5568; + color: #e2e8f0; + border-radius: 4px; + transition: all 0.2s ease-in-out; +} + +.fc .fc-button-group > .fc-button:hover { + background-color: #64748b; +} + +.fc .fc-button-primary:not(:disabled).fc-button-active, +.fc .fc-button-primary:not(:disabled):active { + background-color: #4299e1; + border-color: #4299e1; + color: #fff; + box-shadow: none; +} + +/* Day cells */ +.fc-daygrid-day { + background-color: #2d3748; + border: 1px solid #4a5568; + border-radius: 4px; +} + +.fc-day-other { + background-color: #202c3c !important; + color: #718096; +} + +.fc-day-today { + background-color: #38a169 !important; + border-color: #38a169 !important; +} + +.fc-daygrid-day-number { + font-weight: 500; + color: #e2e8f0; +} + +/* Event styling */ +.fc-event { + border-radius: 4px; + padding: 3px 6px; + font-size: 12px; + color: #ffffff !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +/* Event colors (you can adjust these in your Django template) */ +/* .fc-event-completed { background-color: #38a169; border-color: #38a169; } */ +/* .fc-event-canceled { background-color: #e53e3e; border-color: #e53e3e; } */ +/* .fc-event-scheduled { background-color: #4299e1; border-color: #4299e1; } */ + + +/* List group styling */ +.list-group-item { + border-color: #4a5568; + background-color: #2d3748; + color: #e2e8f0; + transition: background-color 0.2s ease-in-out; +} + +.list-group-item:hover { + background-color: #4a5568; +} + +.modal-content { + background-color: #2d3748; + color: #e2e8f0; +} + +.modal-header .close { + color: #e2e8f0; +} + +/* Responsive adjustments */ +@media (max-width: 767.98px) { + .fc .fc-toolbar-title { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/staticfiles/css/calendar_light.css b/staticfiles/css/calendar_light.css new file mode 100644 index 00000000..e0a47a89 --- /dev/null +++ b/staticfiles/css/calendar_light.css @@ -0,0 +1,99 @@ +/* static/css/light_theme.css */ + +/* Card and container styling */ +.card { + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid #e0e6ed; +} + +/* FullCalendar header */ +.fc .fc-toolbar.fc-header-toolbar { + margin-bottom: 1.5em; +} + +.fc .fc-toolbar-title { + font-size: 1.5rem; + font-weight: 600; + color: #2c3e50; +} + +/* Calendar buttons */ +.fc .fc-button-group > .fc-button { + background-color: #e9ecef; + border-color: #e9ecef; + color: #495057; + border-radius: 4px; + transition: all 0.2s ease-in-out; +} + +.fc .fc-button-group > .fc-button:hover { + background-color: #e2e6ea; +} + +.fc .fc-button-primary:not(:disabled).fc-button-active, +.fc .fc-button-primary:not(:disabled):active { + background-color: #007bff; + border-color: #007bff; + color: #fff; + box-shadow: none; +} + +/* Day cells */ +.fc-daygrid-day { + background-color: #ffffff; + border: 1px solid #e0e6ed; + border-radius: 4px; +} + +.fc-day-other { + background-color: #f8f9fa !important; + color: #ced4da; +} + +.fc-day-today { + background-color: #fff3cd !important; + border-color: #ffeeba !important; +} + +.fc-daygrid-day-number { + font-weight: 500; +} + +/* Event styling */ +.fc-event { + border-radius: 4px; + padding: 3px 6px; + font-size: 12px; + color: #ffffff !important; + text-shadow: 0 1px 1px rgba(0,0,0,0.2); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* List group styling */ +.list-group-item { + border-color: #e0e6ed; + background-color: #ffffff; + transition: background-color 0.2s ease-in-out; +} + +.list-group-item:hover { + background-color: #f8f9fa; +} + +.modal-content { + background-color: #ffffff; + color: #34495e; +} + +.modal-header .close { + color: #adb5bd; +} + +/* Responsive adjustments */ +@media (max-width: 767.98px) { + .fc .fc-toolbar-title { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/staticfiles/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg b/staticfiles/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg new file mode 100644 index 00000000..4b267969 Binary files /dev/null and b/staticfiles/images/customers/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg differ diff --git a/staticfiles/images/logos/vendors/logo1.jpg b/staticfiles/images/logos/vendors/logo1.jpg new file mode 100644 index 00000000..c7fa991e Binary files /dev/null and b/staticfiles/images/logos/vendors/logo1.jpg differ diff --git a/templates/admin_management/management.html b/templates/admin_management/management.html index d79103e3..efd75f84 100644 --- a/templates/admin_management/management.html +++ b/templates/admin_management/management.html @@ -3,6 +3,7 @@ {% block title %} {% trans 'Admin Management' %} {% endblock %} {% block content %} +

{% trans "Admin Management" %}
  • diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index 5eb7b3ad..077b07e9 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -203,7 +203,7 @@ {% if perms.django_ledger.change_billmodel %} {% if "update" not in request.path %} - @@ -225,16 +225,18 @@ {% endif %} - {% if bill.can_approve and perms.django_ledger.can_approve_billmodel %} - - {% endif %} - {% if bill.can_approve and not request.is_manager %} + {% endif %} + {% if bill.can_approve and request.is_accountant %} + {% else %} + {% if bill.can_approve and perms.django_ledger.can_approve_billmodel %} + + {% endif %} {% endif %} {% if bill.can_pay %} diff --git a/templates/crm/notifications_history.html b/templates/crm/notifications_history.html index 764226ae..98649969 100644 --- a/templates/crm/notifications_history.html +++ b/templates/crm/notifications_history.html @@ -30,19 +30,11 @@ {% endfor %}
    - + {% if page_obj.paginator.num_pages > 1 %} +
    +
    {% include 'partials/pagination.html' %}
    +
    + {% endif %} {% else %}

    No notifications found.

    {% endif %} diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index c09af69e..8de794ef 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load i18n static humanize %} +{% load crispy_forms_tags %} {% block title %} {{ _("Opportunity Detail") }} {% endblock title %} @@ -39,8 +40,9 @@ href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity
  • - Update Stage + Update Stage
  • {% endif %} {% if perms.inventory.delete_opportunity %} @@ -1095,6 +1097,36 @@ + + {% include 'modal/delete_modal.html' %} diff --git a/templates/crm/opportunities/opportunity_list.html b/templates/crm/opportunities/opportunity_list.html index c1f17a56..65c45ab2 100644 --- a/templates/crm/opportunities/opportunity_list.html +++ b/templates/crm/opportunities/opportunity_list.html @@ -5,13 +5,28 @@ {{ _("Opportunities") }} {% endblock title %} {% block content %} + {% if opportunities or request.GET.q%}
    -
    -

    - {{ _("Opportunities") }} -
  • -

    +
    +
    +
    +

    + {{ _("Opportunities") }} +
  • +

    +
    +
    +
    + {% if perms.inventory.add_opportunity %} + + {% endif %} +
    @@ -19,29 +34,38 @@
    - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/mail/remind_expire_body.txt b/templates/mail/remind_expire_body.txt index e795fcdf..040dd7d8 100644 --- a/templates/mail/remind_expire_body.txt +++ b/templates/mail/remind_expire_body.txt @@ -7,9 +7,4 @@ http://{{ site_domain }}{% url 'current_plan' %} {% blocktrans %}or you can upgrade your plan here:{% endblocktrans %} -http://{{ site_domain }}{% url 'upgrade_plan' %} - -{% trans "Thank you" %} --- -{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %} -{% endautoescape %} +http://{{ site_domain }}{% url 'upgrade_plan' %} \ No newline at end of file diff --git a/templates/purchase_orders/po_list.html b/templates/purchase_orders/po_list.html index bb51e728..1d760d7b 100644 --- a/templates/purchase_orders/po_list.html +++ b/templates/purchase_orders/po_list.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load i18n static %} -{% block title %}Purchase Orders - {{ block.super }}{% endblock %} +{% block title %}Purchase Orders{% endblock %} {% block content %} @@ -123,7 +123,15 @@ {% include 'modal/delete_modal.html' %} {% else %} - {% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %} - {% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %} + {% if vendors %} + {% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %} + {% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %} + {% else %} + + {% url "vendor_create" request.dealer.slug as vendor_create_url %} + {% include "message-illustration.html" with value1=_("You don't have a Vendor, Please add a Vendor before creating a Purchase Order.") value2=_("Create New Vendor") url=vendor_create_url %} + + {% endif %} + {% endif %} {% endblock %} diff --git a/templates/staff/staff_detail.html b/templates/staff/staff_detail.html new file mode 100644 index 00000000..c8cba830 --- /dev/null +++ b/templates/staff/staff_detail.html @@ -0,0 +1,107 @@ +{% extends 'base.html' %} +{% load i18n static custom_filters crispy_forms_filters %} +{% block title %} + {% trans 'Profile' %} {% endblock %} + {% block content %} +
    +
    +
    +

    {% trans 'Profile' %}

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

    {{ staff.get_local_name }}

    +

    {{staff.user.groups.name}}

    +

    {% trans 'Joined' %} {{ staff.created|timesince }} {% trans 'ago' %}

    +
    +
    +
    +
    +
    +
    +
    {% trans 'last login'|capfirst %}
    +

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

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +

    {% trans 'Default Address' %}

    +
    +
    +
    +
    +
    {% trans 'Address' %}
    +
    +
    +

    {{ staff.address }}

    +
    +
    +
    +
    +
    +
    +
    {% trans 'Email' %}
    +
    +
    {{ staff.user.email }}
    +
    +
    +
    +
    {% trans 'Phone' %}
    +
    +
    {{ staff.phone_number }}
    +
    +
    +
    +
    +
    +
    + +
    + {% endblock %} + + + \ No newline at end of file diff --git a/templates/users/user_detail.html b/templates/users/user_detail.html index 9246bfc9..cd3da0bf 100644 --- a/templates/users/user_detail.html +++ b/templates/users/user_detail.html @@ -79,12 +79,12 @@