diff --git a/inventory/management/commands/set_custom_permissions.py b/inventory/management/commands/set_custom_permissions.py index 23e4a9da..b89f0c45 100644 --- a/inventory/management/commands/set_custom_permissions.py +++ b/inventory/management/commands/set_custom_permissions.py @@ -1,16 +1,16 @@ -from inventory import models +from inventory.models import Lead,Car from django.contrib.auth.models import Permission from django.core.management.base import BaseCommand from django.contrib.contenttypes.models import ContentType from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel - class Command(BaseCommand): def handle(self, *args, **kwargs): - Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(models.Lead)) + Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(Lead)) + Permission.objects.get_or_create(name="Can reassign lead",codename="can_reassign_lead",content_type=ContentType.objects.get_for_model(Lead)) Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel)) Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel)) - Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(models.Car)) + Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(Car)) Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel)) Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel)) Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel)) \ No newline at end of file diff --git a/inventory/models.py b/inventory/models.py index 96459291..ee8a5dfb 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1836,7 +1836,9 @@ class Schedule(models.Model): ("completed", _("Completed")), ("canceled", _("Canceled")), ] - lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="schedules") + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') customer = models.ForeignKey( CustomerModel, on_delete=models.CASCADE, @@ -2634,6 +2636,7 @@ class CustomGroup(models.Model): "notes", "tasks", "activity", + "poitemsuploaded" ], ) self.set_permissions( @@ -2652,7 +2655,7 @@ class CustomGroup(models.Model): elif self.name == "Sales": self.set_permissions( app="django_ledger", - allowed_models=["estimatemodel", "invoicemodel", "customermodel"], + allowed_models=["invoicemodel", "customermodel"], ) self.set_permissions( app="inventory", @@ -2668,6 +2671,10 @@ class CustomGroup(models.Model): "organization", "notes", "tasks", +<<<<<<< HEAD +======= + "lead" +>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11 "activity", ], other_perms=[ @@ -2679,6 +2686,10 @@ class CustomGroup(models.Model): "can_view_inventory", "can_view_sales", "can_view_crm", + "view_estimatemodel", + "add_estimatemodel", + "change_estimatemodel", + "delete_estimatemodel", ], ) ###################################### @@ -2694,7 +2705,13 @@ class CustomGroup(models.Model): "notes", "tasks", "activity", +<<<<<<< HEAD "vendor"], +======= + "vendor", + "poitemsuploaded" + ], +>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11 other_perms=[ "view_car", "view_carlocation", @@ -2711,7 +2728,6 @@ class CustomGroup(models.Model): "bankaccountmodel", "accountmodel", "chartofaccountmodel", - "billmodel", "itemmodel", "invoicemodel", "vendormodel", @@ -2723,7 +2739,7 @@ class CustomGroup(models.Model): "ledgermodel", "transactionmodel" ], - other_perms=["view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"], + other_perms=["view_billmodel","add_billmodel","change_billmodel","delete_billmodel","view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"], ) @@ -2919,6 +2935,8 @@ class PoItemsUploaded(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def get_name(self): + return self.item.item.name.split('||') class ExtraInfo(models.Model): """ Stores additional information for any model with: diff --git a/inventory/override.py b/inventory/override.py new file mode 100644 index 00000000..ccf3ffbe --- /dev/null +++ b/inventory/override.py @@ -0,0 +1,644 @@ +import logging +from django.core.exceptions import ImproperlyConfigured,ValidationError +from django.contrib.auth.mixins import LoginRequiredMixin,PermissionRequiredMixin +from django_ledger.forms.bill import ( + BillModelCreateForm, + BaseBillModelUpdateForm, + DraftBillModelUpdateForm, + get_bill_itemtxs_formset_class, + BillModelConfigureForm, + InReviewBillModelUpdateForm, + ApprovedBillModelUpdateForm, + AccruedAndApprovedBillModelUpdateForm, + PaidBillModelUpdateForm +) +from django.http import HttpResponseForbidden +from django.utils.html import format_html +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django_ledger.models import ItemTransactionModel +from django.views.generic.detail import DetailView +from django_ledger.forms.purchase_order import (ApprovedPurchaseOrderModelUpdateForm, + BasePurchaseOrderModelUpdateForm, + DraftPurchaseOrderModelUpdateForm, + ReviewPurchaseOrderModelUpdateForm, + get_po_itemtxs_formset_class) +from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn +from django_ledger.models import PurchaseOrderModel,EstimateModel,BillModel +from django.views.generic.detail import SingleObjectMixin +from django.views.generic.edit import UpdateView +from django.views.generic.base import RedirectView +from .models import Dealer + + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +class PurchaseOrderModelUpdateView(LoginRequiredMixin, + PermissionRequiredMixin, + UpdateView): + slug_url_kwarg = 'po_pk' + slug_field = 'uuid' + context_object_name = 'po_model' + template_name = "purchase_orders/po_update.html" + context_object_name = "po_model" + permission_required = "django_ledger.change_purchaseordermodel" + extra_context = { + 'header_subtitle_icon': 'uil:bill' + } + action_update_items = False + queryset = None + + def get_context_data(self, itemtxs_formset=None, **kwargs): + dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) + context = super().get_context_data(**kwargs) + context["entity_slug"] = dealer.entity.slug + po_model: PurchaseOrderModel = self.object + if not itemtxs_formset: + itemtxs_qs = self.get_po_itemtxs_qs(po_model) + itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs) + po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) + itemtxs_formset = po_itemtxs_formset_class( + entity_slug=dealer.entity.slug, + user_model=dealer.entity.admin, + po_model=po_model, + queryset=itemtxs_qs, + ) + else: + itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data() + + context["itemtxs_qs"] = itemtxs_qs + context["itemtxs_formset"] = itemtxs_formset + return context + + def get_queryset(self): + dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) + if self.queryset is None: + self.queryset = PurchaseOrderModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=dealer.entity.admin + ).select_related('entity', 'ce_model') + return super().get_queryset() + def get_success_url(self): + return reverse( + "purchase_order_update", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "po_pk": self.kwargs["po_pk"], + }, + ) + def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): + if self.action_update_items: + return HttpResponseRedirect( + redirect_to=reverse( + "purchase_order_update", + kwargs={ + "dealer_slug": dealer_slug, + "entity_slug": entity_slug, + "po_pk": po_pk, + }, + ) + ) + return super(PurchaseOrderModelUpdateView, self).get( + request, dealer_slug, entity_slug, po_pk, *args, **kwargs + ) + + def post(self, request, dealer_slug, entity_slug, *args, **kwargs): + if self.action_update_items: + if not request.user.is_authenticated: + return HttpResponseForbidden() + queryset = self.get_queryset() + po_model: PurchaseOrderModel = self.get_object(queryset=queryset) + self.object = po_model + po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) + itemtxs_formset = po_itemtxs_formset_class( + request.POST, + user_model=request.dealer.entity.admin, + po_model=po_model, + entity_slug=entity_slug, + ) + + if itemtxs_formset.has_changed(): + if itemtxs_formset.is_valid(): + itemtxs_list = itemtxs_formset.save(commit=False) + create_bill_uuids = [ + str(i["uuid"].uuid) + for i in itemtxs_formset.cleaned_data + if i and i["create_bill"] is True + ] + + if create_bill_uuids: + item_uuids = ",".join(create_bill_uuids) + redirect_url = reverse( + "bill-create-po", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "po_pk": po_model.uuid, + }, + ) + redirect_url += f"?item_uuids={item_uuids}" + return HttpResponseRedirect(redirect_url) + + for itemtxs in itemtxs_list: + if not itemtxs.po_model_id: + itemtxs.po_model_id = po_model.uuid + itemtxs.clean() + + itemtxs_list = itemtxs_formset.save() + po_model.update_state() + po_model.clean() + po_model.save( + update_fields=["po_amount", "po_amount_received", "updated"] + ) + # if valid get saved formset from DB + messages.add_message( + request, messages.SUCCESS, "PO items updated successfully." + ) + return self.render_to_response(context=self.get_context_data()) + # if not valid, return formset with errors... + return self.render_to_response( + context=self.get_context_data(itemtxs_formset=itemtxs_formset) + ) + return super(PurchaseOrderModelUpdateView, self).post( + request, dealer_slug, entity_slug, *args, **kwargs + ) + + def get_form(self, form_class=None): + po_model: PurchaseOrderModel = self.object + dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) + if po_model.is_draft(): + return DraftPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs["entity_slug"], + user_model=dealer.entity.admin, + **self.get_form_kwargs(), + ) + elif po_model.is_review(): + return ReviewPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs["entity_slug"], + user_model=dealer.entity.admin, + **self.get_form_kwargs(), + ) + elif po_model.is_approved(): + return ApprovedPurchaseOrderModelUpdateForm( + entity_slug=self.kwargs["entity_slug"], + user_model=dealer.entity.admin, + **self.get_form_kwargs(), + ) + return BasePurchaseOrderModelUpdateForm( + entity_slug=self.kwargs["entity_slug"], + user_model=dealer.entity.admin, + **self.get_form_kwargs(), + ) + + def get_form_kwargs(self): + if self.action_update_items: + return { + 'initial': self.get_initial(), + 'prefix': self.get_prefix(), + 'instance': self.object + } + return super(PurchaseOrderModelUpdateView, self).get_form_kwargs() + + + def get_po_itemtxs_qs(self, po_model: PurchaseOrderModel): + return po_model.itemtransactionmodel_set.select_related('bill_model', 'po_model').order_by('created') + + def form_valid(self, form: BasePurchaseOrderModelUpdateForm): + po_model: PurchaseOrderModel = form.save(commit=False) + + if form.has_changed(): + po_items_qs = ItemTransactionModel.objects.for_po( + entity_slug=self.kwargs['entity_slug'], + user_model=dealer.entity.admin, + po_pk=po_model.uuid, + ).select_related('bill_model') + + if all(['po_status' in form.changed_data, + po_model.po_status == po_model.PO_STATUS_APPROVED]): + po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_NOT_ORDERED) + + if 'fulfilled' in form.changed_data: + + if not all([i.bill_model for i in po_items_qs]): + messages.add_message(self.request, + messages.ERROR, + f'All PO items must be billed before marking' + f' PO: {po_model.po_number} as fulfilled.', + extra_tags='is-danger') + return self.get(self.request) + + else: + if not all([i.bill_model.is_paid() for i in po_items_qs]): + messages.add_message(self.request, + messages.SUCCESS, + f'All bills must be paid before marking' + f' PO: {po_model.po_number} as fulfilled.', + extra_tags='is-success') + return self.get(self.request) + + po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_RECEIVED) + + messages.add_message(self.request, + messages.SUCCESS, + f'{self.object.po_number} successfully updated.', + extra_tags='is-success') + + return super().form_valid(form) + + + +class BasePurchaseOrderActionActionView(LoginRequiredMixin, + PermissionRequiredMixin, + RedirectView, + SingleObjectMixin): + http_method_names = ['get'] + pk_url_kwarg = 'po_pk' + action_name = None + commit = True + permission_required = None + queryset = None + + def get_queryset(self): + dealer = get_object_or_404(Dealer, slug=self.kwargs['dealer_slug']) + if self.queryset is None: + self.queryset = PurchaseOrderModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=dealer.entity.admin + ).select_related('entity', 'ce_model') + return super().get_queryset() + + def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs): + return reverse( + "purchase_order_update", + kwargs={ + "dealer_slug": dealer_slug, + "entity_slug": entity_slug, + "po_pk": po_pk, + }, + ) + + def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): + # kwargs["user_model"] = dealer.entity.admin + # Get user information for logging + user_username = request.user.username if request.user.is_authenticated else 'anonymous' + + dealer = get_object_or_404(Dealer, slug=dealer_slug) + kwargs["user_model"] = dealer.entity.admin + if not self.action_name: + raise ImproperlyConfigured("View attribute action_name is required.") + response = super(BasePurchaseOrderActionActionView, self).get( + request, dealer_slug, entity_slug, po_pk, *args, **kwargs + ) + po_model: PurchaseOrderModel = self.get_object() + + # Log the attempt to perform the action + logger.debug( + f"User {user_username} attempting to call action '{self.action_name}' " + f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." + ) + try: + getattr(po_model, self.action_name)(commit=self.commit, **kwargs) + # --- Single-line log for successful action --- + logger.info( + f"User {user_username} successfully executed action '{self.action_name}' " + f"on Purchase Order ID: {po_model.pk}." + ) + messages.add_message( + request, + message="PO updated successfully.", + level=messages.SUCCESS, + ) + except ValidationError as e: + # --- Single-line log for ValidationError --- + logger.warning( + f"User {user_username} encountered a validation error " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + print(e) + return response + +class BillModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): + slug_url_kwarg = 'bill_pk' + slug_field = 'uuid' + context_object_name = 'bill' + template_name = "bill/bill_detail.html" + extra_context = { + 'header_subtitle_icon': 'uil:bill', + 'hide_menu': True + } + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=object_list, **kwargs) + context["dealer"] = self.request.dealer + bill_model: BillModel = self.object + title = f'Bill {bill_model.bill_number}' + context['page_title'] = title + context['header_title'] = title + + bill_model: BillModel = self.object + bill_items_qs, item_data = bill_model.get_itemtxs_data() + context['itemtxs_qs'] = bill_items_qs + context['total_amount__sum'] = item_data['total_amount__sum'] + + if not bill_model.is_configured(): + link = format_html(f""" + here + """) + msg = f'Bill {bill_model.bill_number} has not been fully set up. ' + \ + f'Please update or assign associated accounts {link}.' + messages.add_message(self.request, + message=msg, + level=messages.WARNING, + extra_tags='is-danger') + return context + + + def get_queryset(self): + dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) + if self.queryset is None: + entity_model = dealer.entity + qs = entity_model.get_bills() + self.queryset = qs + return super().get_queryset() + + +######################################################3 +#BILL + +class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): + slug_url_kwarg = 'bill_pk' + slug_field = 'uuid' + context_object_name = 'bill_model' + template_name = "bill/bill_update.html" + extra_context = { + 'header_subtitle_icon': 'uil:bill' + } + http_method_names = ['get', 'post'] + action_update_items = False + queryset = None + + def get_queryset(self): + dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) + if self.queryset is None: + entity_model = dealer.entity + qs = entity_model.get_bills() + self.queryset = qs + return super().get_queryset().select_related( + 'ledger', + 'ledger__entity', + 'vendor', + 'cash_account', + 'prepaid_account', + 'unearned_account', + 'cash_account__coa_model', + 'prepaid_account__coa_model', + 'unearned_account__coa_model' + ) + + def get_form(self, form_class=None): + form_class = self.get_form_class() + dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) + entity_model = dealer.entity + if self.request.method == 'POST' and self.action_update_items: + return form_class( + entity_model=entity_model, + user_model=dealer.entity.admin, + instance=self.object + ) + return form_class( + entity_model=entity_model, + user_model=dealer.entity.admin, + **self.get_form_kwargs() + ) + + def get_form_class(self): + bill_model: BillModel = self.object + if not bill_model.is_configured(): + return BillModelConfigureForm + if bill_model.is_draft(): + return DraftBillModelUpdateForm + elif bill_model.is_review(): + return InReviewBillModelUpdateForm + elif bill_model.is_approved() and not bill_model.accrue: + return ApprovedBillModelUpdateForm + elif bill_model.is_approved() and bill_model.accrue: + return AccruedAndApprovedBillModelUpdateForm + elif bill_model.is_paid(): + return PaidBillModelUpdateForm + return BaseBillModelUpdateForm + + def get_context_data(self, + *, + object_list=None, + itemtxs_formset=None, + **kwargs): + + context = super().get_context_data(object_list=object_list, **kwargs) + dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug']) + entity_model = dealer.entity + bill_model: BillModel = self.object + ledger_model = bill_model.ledger + + title = f'Bill {bill_model.bill_number}' + context['page_title'] = title + context['header_title'] = title + context['header_subtitle'] = bill_model.get_bill_status_display() + + if not bill_model.is_configured(): + messages.add_message( + request=self.request, + message=f'Bill {bill_model.bill_number} must have all accounts configured.', + level=messages.ERROR, + extra_tags='is-danger' + ) + + if not bill_model.is_paid(): + if ledger_model.locked: + messages.add_message(self.request, + messages.ERROR, + f'Warning! This bill is locked. Must unlock before making any changes.', + extra_tags='is-danger') + + if ledger_model.locked: + messages.add_message(self.request, + messages.ERROR, + f'Warning! This bill is locked. Must unlock before making any changes.', + extra_tags='is-danger') + + if not ledger_model.is_posted(): + messages.add_message(self.request, + messages.INFO, + f'This bill has not been posted. Must post to see ledger changes.', + extra_tags='is-info') + + itemtxs_qs = itemtxs_formset.get_queryset() if itemtxs_formset else None + if not itemtxs_formset: + itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) + itemtxs_formset = itemtxs_formset_class(entity_model=entity_model, bill_model=bill_model) + itemtxs_qs, itemtxs_agg = bill_model.get_itemtxs_data(queryset=itemtxs_qs) + + has_po = any(i.po_model_id for i in itemtxs_qs) + + if has_po: + itemtxs_formset.can_delete = False + itemtxs_formset.has_po = has_po + + context['itemtxs_formset'] = itemtxs_formset + context['total_amount__sum'] = itemtxs_agg['total_amount__sum'] + context['has_po'] = has_po + return context + + def get_success_url(self): + return reverse( + "bill-update", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "bill_pk": self.kwargs["bill_pk"], + }, + ) + def form_valid(self, form): + form.save(commit=False) + messages.add_message(self.request, + messages.SUCCESS, + f'Bill {self.object.bill_number} successfully updated.', + extra_tags='is-success') + return super().form_valid(form) + + def get(self, request,dealer_slug,entity_slug,bill_pk, *args, **kwargs): + if self.action_update_items: + return HttpResponseRedirect( + redirect_to=reverse('bill-update', + kwargs={ + 'dealer_slug': dealer_slug, + 'entity_slug': entity_slug, + 'bill_pk': bill_pk + }) + ) + return super(BillModelUpdateView, self).get(request, *args, **kwargs) + + def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs): + if self.action_update_items: + if not request.user.is_authenticated: + return HttpResponseForbidden() + + queryset = self.get_queryset() + dealer = get_object_or_404(Dealer, slug=dealer_slug) + entity_model = dealer.entity + bill_model: BillModel = self.get_object(queryset=queryset) + bill_pk = bill_model.uuid + + self.object = bill_model + + bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) + itemtxs_formset = bill_itemtxs_formset_class( + request.POST, bill_model=bill_model, entity_model=entity_model + ) + + if itemtxs_formset.has_changed(): + if itemtxs_formset.is_valid(): + itemtxs_list = itemtxs_formset.save(commit=False) + + for itemtxs in itemtxs_list: + itemtxs.bill_model_id = bill_model.uuid + itemtxs.clean() + + itemtxs_formset.save() + itemtxs_qs = bill_model.update_amount_due() + bill_model.get_state(commit=True) + bill_model.clean() + bill_model.save( + update_fields=[ + "amount_due", + "amount_receivable", + "amount_unearned", + "amount_earned", + "updated", + ] + ) + + bill_model.migrate_state( + entity_slug=self.kwargs["entity_slug"], + user_model=self.request.user, + itemtxs_qs=itemtxs_qs, + raise_exception=False, + ) + + messages.add_message( + request, + message=f"Items for Invoice {bill_model.bill_number} saved.", + level=messages.SUCCESS, + ) + + # if valid get saved formset from DB + return HttpResponseRedirect( + redirect_to=reverse( + "bill-update", + kwargs={ + "dealer_slug": dealer_slug, + "entity_slug": entity_model.slug, + "bill_pk": bill_pk, + }, + ) + ) + context = self.get_context_data(itemtxs_formset=itemtxs_formset) + return self.render_to_response(context=context) + return super(BillModelUpdateView, self).post( + request, dealer_slug, entity_slug, bill_pk, **kwargs + ) + + + +class BaseBillActionView(LoginRequiredMixin,PermissionRequiredMixin, RedirectView, SingleObjectMixin): + http_method_names = ['get'] + pk_url_kwarg = 'bill_pk' + action_name = None + commit = True + permission_required = "django_ledger.change_billmodel" + queryset = None + + def get_queryset(self): + if self.queryset is None: + dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) + entity_model = dealer.entity + qs = entity_model.get_bills() + self.queryset = qs + return super().get_queryset() + + + def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs): + return reverse( + "bill-update", + kwargs={ + "dealer_slug": dealer_slug, + "entity_slug": entity_slug, + "bill_pk": bill_pk, + }, + ) + + + def get(self, request, *args, **kwargs): + dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"]) + kwargs['user_model'] = dealer.entity.admin + if not self.action_name: + raise ImproperlyConfigured('View attribute action_name is required.') + response = super(BaseBillActionView, self).get(request, *args, **kwargs) + bill_model: BillModel = self.get_object() + + try: + getattr(bill_model, self.action_name)(commit=self.commit, **kwargs) + except ValidationError as e: + messages.add_message(request, + message=e.message, + level=messages.ERROR, + extra_tags='is-danger') + return response diff --git a/inventory/signals.py b/inventory/signals.py index 652581d7..43cf2d93 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -17,7 +17,8 @@ from django_ledger.models import ( LedgerModel, AccountModel, PurchaseOrderModel, - EstimateModel + EstimateModel, + BillModel ) from . import models from django.utils.timezone import now @@ -954,7 +955,7 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs): models.Notification.objects.create( user=accountant, message=f""" - New Purchase Order {instance.po_number} has been added to dealer {instance.dealer.name}. + New Purchase Order {instance.po_number} has been added to dealer {dealer.name}. View """, ) @@ -975,7 +976,10 @@ def car_created_notification(sender, instance, created, **kwargs): def po_fullfilled_notification(sender, instance, created, **kwargs): if instance.is_fulfilled(): dealer = models.Dealer.objects.get(entity=instance.entity) - recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all() + recipients = User.objects.filter( + groups__customgroup__dealer=instance.dealer, + groups__customgroup__name__in=["Manager", "Inventory"] + ) for recipient in recipients: models.Notification.objects.create( user=recipient, @@ -987,14 +991,16 @@ def po_fullfilled_notification(sender, instance, created, **kwargs): @receiver(post_save, sender=models.Vendor) def vendor_created_notification(sender, instance, created, **kwargs): if created: - recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all() + recipients = User.objects.filter( + groups__customgroup__dealer=instance.dealer, + groups__customgroup__name__in=["Manager", "Inventory"] + ) for recipient in recipients: models.Notification.objects.create( user=recipient, message=f""" New Vendor {instance.name} has been added to dealer {instance.dealer.name}. - View """, ) @@ -1055,19 +1061,33 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs): """ ) +@receiver(post_save, sender=BillModel) +def bill_model_in_approve_notification(sender, instance, created, **kwargs): + if instance.is_review(): + dealer = models.Dealer.objects.get(entity=instance.ledger.entity) + recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email) -# @receiver(post_save, sender=models.Lead) -# def lead_created_notification(sender, instance, created, **kwargs): -# if created: -# models.Notification.objects.create( -# user=instance.staff.user, -# message=f""" -# New Lead has been added. -# View -# """, -# ) + for recipient in recipients: + models.Notification.objects.create( + user=recipient, + message=f""" + Bill {instance.bill_number} is in review,please review and approve it + View. + """ + ) +@receiver(post_save, sender=BillModel) +def bill_model_after_approve_notification(sender, instance, created, **kwargs): + if instance.is_approved(): + dealer = models.Dealer.objects.get(entity=instance.ledger.entity) + recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Accountant").first().group.user_set.exclude(email=dealer.user.email) -# send notification after car is sold {manager,dealer} -# after po review send notification to {manager} to approve po -# after estimate review send notification to {manager} to approve estimate \ No newline at end of file + for recipient in recipients: + models.Notification.objects.create( + user=recipient, + message=f""" + Bill {instance.bill_number} has been approved. + View. + please comlete the bill payment. + """ + ) \ No newline at end of file diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 3390919a..5b449803 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -454,14 +454,16 @@ def po_item_table1(context, queryset): @register.inclusion_tag( "purchase_orders/includes/po_item_formset.html", takes_context=True ) -def po_item_formset_table(context, po_model, itemtxs_formset): +def po_item_formset_table(context, po_model, itemtxs_formset,user): # print(len(itemtxs_formset.forms)) - for form in itemtxs_formset.forms: form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter( item_role="inventory" ) + return { + "can_add_bill": user.has_perm("django_ledger.add_billmodel"), + "can_view_bill": user.has_perm("django_ledger.view_billmodel"), "dealer_slug": context["view"].kwargs["dealer_slug"], "entity_slug": context["view"].kwargs["entity_slug"], "po_model": po_model, diff --git a/inventory/urls.py b/inventory/urls.py index 25956e4e..1f8e5084 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -141,9 +141,9 @@ urlpatterns = [ name="send_lead_email_with_template", ), path( - "/crm/leads//schedule/", - views.schedule_lead, - name="schedule_lead", + "/crm///schedule/", + views.schedule_event, + name="schedule_event", ), path( "/crm/leads/schedule//cancel/", @@ -802,17 +802,17 @@ urlpatterns = [ ), path( "/items/bills//detail//", - views.BillModelDetailViewView.as_view(), + views.BillModelDetailView.as_view(), name="bill-detail", ), path( "/items/bills//update//", - views.BillModelUpdateViewView.as_view(), + views.BillModelUpdateView.as_view(), name="bill-update", ), path( "/items/bills//update//items/", - views.BillModelUpdateViewView.as_view(action_update_items=True), + views.BillModelUpdateView.as_view(action_update_items=True), name="bill-update-items", ), ############################################################ diff --git a/inventory/utils.py b/inventory/utils.py index 9a0438bd..08cb62ce 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1288,7 +1288,7 @@ def handle_account_process(invoice, amount, finance_data): except Exception as e: logger.error( f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}", - exc_info=True + exc_info=True ) print(e) @@ -1419,12 +1419,23 @@ def handle_payment(request, order): headers = {"Content-Type": "application/json", "Accept": "application/json"} auth = (settings.MOYASAR_SECRET_KEY, "") response = requests.request("POST", url, auth=auth, headers=headers, data=payload) + if response.status_code == 400: + data = response.json() + if data["type"] == "validation_error": + errors = data.get("errors", {}) + if "source.year" in errors: + raise Exception("Invalid expiry year") + else: + raise Exception("Validation Error: ", errors) + else: + print("Failed to process payment:", data) # order.status = AbstractOrder.STATUS.NEW order.save() # data = response.json() amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100))) + print(data) models.PaymentHistory.objects.create( user=request.user, user_data=user_data, diff --git a/inventory/views.py b/inventory/views.py index d79db58d..71a5d33f 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -78,9 +78,6 @@ from django.views.generic import ( from django.db.models import Case, Value, IntegerField, When -#logger -logger=logging.getLogger(__name__) - # Django Ledger from django_ledger.io import roles from django_ledger.utils import accruable_net_summary @@ -107,9 +104,9 @@ from django_ledger.forms.bank_account import ( ) from django_ledger.views.bill import ( # BillModelCreateView, - BillModelDetailView, - BillModelUpdateView, - BaseBillActionView as BaseBillActionViewBase, + # BillModelDetailView, + # BillModelUpdateView, + # BaseBillActionView as BaseBillActionViewBase, BillModelModelBaseView, ) from django_ledger.forms.bill import ( @@ -136,10 +133,18 @@ from django_ledger.forms.purchase_order import ( ) from django_ledger.views.purchase_order import ( PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase, - PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, - BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, + # PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, + # BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase, ) +from .override import ( + PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, + BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, + BillModelDetailView as BillModelDetailViewBase, + BillModelUpdateView as BillModelUpdateViewBase, + BaseBillActionView as BaseBillActionViewBase, + ) + from django_ledger.models import ( ItemTransactionModel, EntityModel, @@ -205,7 +210,6 @@ from django_q.tasks import async_task logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) - class Hash(Func): """ Represents a function used to compute a hash value. @@ -2250,7 +2254,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView @login_required -@permission_required("inventory.add_note", raise_exception=True) +@permission_required("inventory.add_notes", raise_exception=True) def add_note_to_customer(request,dealer_slug, slug): """ This function allows authenticated users to add a note to a specific customer. The @@ -2922,6 +2926,8 @@ def GroupPermissionView(request, dealer_slug, pk): ("inventory", "notes"), ("inventory", "tasks"), ("inventory", "activity"), + ("inventory", "vendor"), + ("inventory", "poitemsuploaded"), ("django_ledger", "purchaseordermodel"), @@ -5619,7 +5625,7 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): context["transfer_form"].fields[ "transfer_to" ].queryset = models.Staff.objects.filter( - dealer=dealer,staff_member__user__groups__permissions__codename__contains="add_lead").exclude(staff_member__user=self.request.user).distinct() + dealer=dealer,staff_member__user__groups__permissions__codename__contains="can_reassign_lead").exclude(staff_member__user=self.request.user).distinct() context["activity_form"] = forms.ActivityForm() context["staff_task_form"] = forms.StaffTaskForm() @@ -5818,7 +5824,7 @@ def update_lead_actions(request,dealer_slug): # Log before updating lead fields logger.debug( - f"User {user_username} found Lead ID: {lead.id} ('{lead.name}') " + f"User {user_username} found Lead ID: {lead.pk} ('{lead.slug}') " f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'." ) @@ -5830,21 +5836,20 @@ def update_lead_actions(request,dealer_slug): next_action_date, "%Y-%m-%dT%H:%M" ) lead.next_action_date = timezone.make_aware(next_action_datetime) - logger.debug(f"Lead ID: {lead.id} next_action_date parsed to {lead.next_action_date}.") + logger.debug(f"Lead ID: {lead.pk} next_action_date parsed to {lead.next_action_date}.") except ValueError as ve: # Log for invalid date format logger.warning( f"submitted invalid date format ('{next_action_date}') " - f"for Lead ID: {lead.id}. Error: {ve}" + f"for Lead ID: {lead.pk}. Error: {ve}" ) return JsonResponse( {"success": False, "message": "Invalid date format"}, status=400 ) - # Save the lead lead.save() # --- Logging for successful update (main try block success) --- logger.info( - f"User {user_username} successfully updated Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). " f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'." ) return JsonResponse( @@ -5932,7 +5937,7 @@ def LeadDeleteView(request,dealer_slug, slug): # Log intent before attempting deletion logger.debug( - f"User {user_username} attempting to delete Lead ID: {lead.id} ('{lead.name}') " + f"User {user_username} attempting to delete Lead ID: {lead.pk} ('{lead.slug}') " f"and its associated customer/user for dealer '{dealer_slug}'." ) @@ -5943,19 +5948,19 @@ def LeadDeleteView(request,dealer_slug, slug): # --- Single-line log for successful associated user/customer deletion --- logger.info( f"User {user_username} successfully deleted associated user and customer " - f"for Lead ID: {lead.id} ('{lead.name}') (Email: {lead.customer.email})." + f"for Lead ID: {lead.pk} ('{lead.slug}') (Email: {lead.customer.email})." ) except Exception as e: # --- Single-line log for error during associated user/customer deletion --- logger.error( f"User {user_username} encountered an error deleting associated user/customer " - f"for Lead ID: {lead.id} ('{lead.name}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email + f"for Lead ID: {lead.slug} ('{lead.slug}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email f"Error: {e}", exc_info=True ) print(e) - lead_id_final = lead.id # Capture before deletion - lead_name_final = lead.name + lead_id_final = lead.pk # Capture before deletion + lead_name_final = lead.slug lead.delete() # Log the final lead deletion, which happens unconditionally after the try-except logger.info( @@ -5967,7 +5972,7 @@ def LeadDeleteView(request,dealer_slug, slug): @login_required -@permission_required("inventory.add_note", raise_exception=True) +@permission_required("inventory.add_notes", raise_exception=True) def add_note_to_lead(request,dealer_slug, slug): """ Adds a note to a specific lead. This view is accessible only to authenticated @@ -6000,7 +6005,7 @@ def add_note_to_lead(request,dealer_slug, slug): @login_required -@permission_required("inventory.add_note", raise_exception=True) +@permission_required("inventory.add_notes", raise_exception=True) def add_note_to_opportunity(request,dealer_slug, slug): """ Add a note to a specific opportunity identified by its primary key. @@ -6033,7 +6038,7 @@ def add_note_to_opportunity(request,dealer_slug, slug): @login_required -@permission_required("inventory.delete_note", raise_exception=True) +@permission_required("inventory.delete_notes", raise_exception=True) def delete_note(request,dealer_slug, pk): """ Deletes a specific note created by the currently logged-in user and redirects @@ -6095,7 +6100,7 @@ def lead_convert(request,dealer_slug, slug): @login_required @permission_required("inventory.add_schedule", raise_exception=True) -def schedule_lead(request, dealer_slug,slug): +def schedule_event(request, dealer_slug,content_type,slug): """ Handles the scheduling of a lead for an appointment. @@ -6113,29 +6118,46 @@ def schedule_lead(request, dealer_slug,slug): method and validity of the form submission. :rtype: HttpResponse """ + user_username = request.user.username if request.user.is_authenticated else 'anonymous' + # Log the attempt to retrieve the model dynamically + logger.debug( + f"User {user_username} attempting to retrieve model " + f"for content_type '{content_type}' for dealer '{dealer_slug}'." + ) + try: + model = apps.get_model(f"inventory.{content_type}") + except LookupError: + # --- Single-line log for LookupError (Model not found) --- + logger.warning( + f"User {user_username} requested an invalid model content_type: '{content_type}'. " + f"Raising Http404 for dealer '{dealer_slug}'." + ) + raise Http404("Model not found") + + dealer = get_object_or_404(models.Dealer,slug=dealer_slug) + obj = get_object_or_404(model, slug=slug) if not request.is_staff: - messages.error(request, _("You do not have permission to schedule lead")) - return redirect("lead_list", dealer_slug=dealer_slug) - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - lead = get_object_or_404(models.Lead, slug=slug, dealer=dealer) - if request.method == "POST": - # Get user info for logging (available throughout the POST handling) - user_username = request.user.username if request.user.is_authenticated else 'anonymous' + messages.error(request, _("You do not have permission to schedule.")) + return redirect(request.META.get("HTTP_REFERER")) + if request.method == "POST": form = forms.ScheduleForm(request.POST) if form.is_valid(): instance = form.save(commit=False) - instance.lead = lead + instance.content_object = obj instance.scheduled_by = request.user - instance.customer = lead.get_customer_model() + if obj.customer: + instance.customer = obj.customer.customer_model + elif obj.organization: + instance.cutsomer = obj.organization.customer_model service = Service.objects.get(name=instance.scheduled_type) # Log attempt to create AppointmentRequest logger.debug( f"User {user_username} attempting to create AppointmentRequest " - f"for Lead ID: {lead.id} ('{lead.name}'). Service: '{service.name}', " + f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', " f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'." ) @@ -6149,41 +6171,34 @@ def schedule_lead(request, dealer_slug,slug): ) except ValidationError as e: messages.error(request, str(e)) - return redirect("schedule_lead", dealer_slug=lead.dealer.slug, slug=lead.slug) + return redirect(request.META.get("HTTP_REFERER")) - client = get_object_or_404(User, email=lead.email) + client = get_object_or_404(User, email=instance.customer.email) # Create Appointment Appointment.objects.create( client=client, appointment_request=appointment_request, - phone=lead.phone_number, - address=lead.address, + phone=instance.phone, + address=instance.address_1, ) instance.save() # --- Logging for successful AppointmentRequest and Appointment creation --- logger.info( - f"User {user_username} successfully scheduled Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). " f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}." ) messages.success(request, _("Appointment Created Successfully")) - try: - if lead.opportunity: - return redirect("opportunity_detail", dealer_slug=lead.dealer.slug, slug=lead.opportunity.slug) - except models.Lead.opportunity.RelatedObjectDoesNotExist: - logger.info( - f"Lead ID: {lead.id} ('{lead.name}') has no associated opportunity. " - f"Redirecting to lead list for user {user_username} " - ) - return redirect("lead_list", dealer_slug=lead.dealer.slug) + + return redirect(request.META.get("HTTP_REFERER")) else: # Log for invalid form data logger.warning( f"User {user_username} submitted invalid schedule form data " - f"for Lead ID: {lead.id} ('{lead.name}'). Errors: {form.errors.as_json()}" + f"for Lead ID: {obj.pk} ('{obj.slug}'). Errors: {form.errors.as_json()}" ) messages.error(request, f"Invalid form data: {str(form.errors)}") - return redirect("schedule_lead", dealer_slug=dealer_slug,slug=lead.slug) + return redirect(request.META.get("HTTP_REFERER")) form = forms.ScheduleForm() return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form}) @@ -6243,7 +6258,6 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) lead = get_object_or_404(models.Lead, slug=slug) status = request.GET.get("status") - # Get user info for logging user_username = request.user.username if request.user.is_authenticated else 'anonymous' if status == "draft": @@ -6264,11 +6278,12 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None): activity_type=models.ActionChoices.EMAIL, ) messages.success(request, _("Email Draft successfully")) + try: - if lead.opportunity: + if getattr(lead, "opportunity",None): # Log success when opportunity exists and redirecting logger.info( - f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." ) response = HttpResponse( @@ -6280,16 +6295,16 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None): else: # Log success when no opportunity and redirecting to lead detail logger.info( - f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " f"Lead has no Opportunity, redirecting to lead detail." ) - response = HttpResponse(redirect("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)) + response = HttpResponse() response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug) return response except models.Lead.opportunity.RelatedObjectDoesNotExist: # --- Log when Lead.opportunity does not exist (Draft status) --- logger.info( - f"User {user_username} drafted email for Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " f"Lead's opportunity does not exist. Redirecting to lead list." ) return redirect("lead_list",dealer_slug=dealer.slug) @@ -6328,14 +6343,14 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None): if lead.opportunity: # Log success when opportunity exists and redirecting after sending email logger.info( - f"User {user_username} successfully sent email for Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). " f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." ) return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug) except models.Lead.opportunity.RelatedObjectDoesNotExist: # --- Log when Lead.opportunity does not exist (POST request for sending) --- logger.info( - f"User {user_username} sent email for Lead ID: {lead.id} ('{lead.name}'). " + f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). " f"Lead's opportunity does not exist. Redirecting to lead list." ) return redirect("lead_list",dealer_slug=dealer_slug) @@ -6393,7 +6408,6 @@ def add_activity_to_lead(request, pk): form = forms.ActivityForm(request.POST) if form.is_valid(): activity = form.save(commit=False) - print(activity) activity.content_object = lead activity.dealer = dealer activity.activity_type = form.cleaned_data["activity_type"] @@ -6451,6 +6465,14 @@ class OpportunityCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateVie instance.lead.save() return super().form_valid(form) + def get_form(self,form_class=None): + dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug")) + staff = getattr(self.request.user.staffmember, "staff", None) + form = super().get_form(form_class) + form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0) + form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff) + return form + def get_success_url(self): return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug}) @@ -6487,8 +6509,9 @@ class OpportunityUpdateView(LoginRequiredMixin,PermissionRequiredMixin, SuccessM def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug")) - form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer) - form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer) + staff = getattr(self.request.user.staffmember, "staff", None) + form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0) + form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff) return form def get_success_url(self): @@ -6530,19 +6553,19 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV content_type__model="lead", object_id=self.object.id ).order_by("-created") context["lead_notes"] = models.Notes.objects.filter( - content_type__model="lead", object_id=self.object.lead.id + content_type__model="lead", object_id=self.object.lead.pk ).order_by("-created") context["notes"] = models.Notes.objects.filter( content_type__model="opportunity", object_id=self.object.id ).order_by("-created") context["lead_activities"] = models.Activity.objects.filter( - content_type__model="lead", object_id=self.object.lead.id + content_type__model="lead", object_id=self.object.lead.pk ) context["activities"] = models.Activity.objects.filter( content_type__model="opportunity", object_id=self.object.id ) lead_email_qs = models.Email.objects.filter( - content_type__model="lead", object_id=self.object.lead.id + content_type__model="lead", object_id=self.object.lead.pk ) email_qs = models.Email.objects.filter( content_type__model="opportunity", object_id=self.object.id @@ -6556,8 +6579,9 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV "draft": lead_email_qs.filter(status="DRAFT"), } context["staff_task_form"] = forms.StaffTaskForm() + context["note_form"] = forms.NoteForm() context["lead_tasks"] = models.Tasks.objects.filter( - content_type__model="lead", object_id=self.object.lead.id + content_type__model="lead", object_id=self.object.lead.pk ) context["tasks"] = models.Tasks.objects.filter( content_type__model="opportunity", object_id=self.object.id @@ -6580,7 +6604,8 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) - queryset = models.Opportunity.objects.filter(dealer=dealer) + staff = getattr(self.request.user.staffmember, "staff", None) + queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) # Search filter search = self.request.GET.get("search") @@ -7040,7 +7065,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") - permission_required = ["django_ledger.add_billmodel"] + permission_required = "django_ledger.add_billmodel" extra_context = { "page_title": PAGE_TITLE, "header_title": PAGE_TITLE, @@ -7048,7 +7073,6 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) } for_purchase_order = False for_estimate = False - permission_required = "django_ledger.add_billmodel" # Get user info for logging @@ -7061,7 +7085,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) if self.for_estimate and "ce_pk" in self.kwargs: estimate_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin ) estimate_model: EstimateModel = get_object_or_404( estimate_qs, uuid__exact=self.kwargs["ce_pk"] @@ -7080,7 +7104,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) def get_context_data(self, **kwargs): context = super(BillModelCreateView, self).get_context_data(**kwargs) - + dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) user_username = self.request.user.username if self.request.user.is_authenticated else 'anonymous' if self.for_purchase_order: @@ -7110,7 +7134,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) return HttpResponseBadRequest() po_qs = PurchaseOrderModel.objects.for_entity( - entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin ).prefetch_related("itemtransactionmodel_set") po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) po_itemtxs_qs = po_model.itemtransactionmodel_set.filter( @@ -7131,7 +7155,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) ) elif self.for_estimate: estimate_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin ) estimate_uuid = self.kwargs["ce_pk"] estimate_model: EstimateModel = get_object_or_404( @@ -7175,7 +7199,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) if self.for_estimate: ce_pk = self.kwargs["ce_pk"] estimate_model_qs = EstimateModel.objects.for_entity( - entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin ) estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk) @@ -7188,7 +7212,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) return HttpResponseBadRequest() item_uuids = item_uuids.split(",") po_qs = PurchaseOrderModel.objects.for_entity( - entity_slug=self.kwargs["entity_slug"], user_model=self.request.user + entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin ) po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) @@ -7259,101 +7283,33 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) ) -class BillModelDetailViewView(BillModelDetailView): +class BillModelDetailView(BillModelDetailViewBase): template_name = "bill/bill_detail.html" permission_required = ["django_ledger.view_billmodel"] def get_context_data(self, **kwargs): - context = super(BillModelDetailViewView, self).get_context_data(**kwargs) + context = super(BillModelDetailView, self).get_context_data(**kwargs) context["dealer"] = self.request.dealer return context + def get_queryset(self): + qs = super().get_queryset() + return qs.select_related( + 'ledger', + 'ledger__entity', + 'vendor', + 'cash_account', + 'prepaid_account', + 'unearned_account', + 'cash_account__coa_model', + 'prepaid_account__coa_model', + 'unearned_account__coa_model' + ) -class BillModelUpdateViewView(BillModelUpdateView): - template_name = "bill/bill_update.html" +class BillModelUpdateView(BillModelUpdateViewBase): permission_required = ["django_ledger.change_billmodel"] - def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs): - if self.action_update_items: - if not request.user.is_authenticated: - return HttpResponseForbidden() - - queryset = self.get_queryset() - entity_model: EntityModel = self.get_authorized_entity_instance() - bill_model: BillModel = self.get_object(queryset=queryset) - bill_pk = bill_model.uuid - - self.object = bill_model - - bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model) - itemtxs_formset = bill_itemtxs_formset_class( - request.POST, bill_model=bill_model, entity_model=entity_model - ) - - if itemtxs_formset.has_changed(): - if itemtxs_formset.is_valid(): - itemtxs_list = itemtxs_formset.save(commit=False) - - for itemtxs in itemtxs_list: - itemtxs.bill_model_id = bill_model.uuid - itemtxs.clean() - - itemtxs_formset.save() - itemtxs_qs = bill_model.update_amount_due() - bill_model.get_state(commit=True) - bill_model.clean() - bill_model.save( - update_fields=[ - "amount_due", - "amount_receivable", - "amount_unearned", - "amount_earned", - "updated", - ] - ) - - bill_model.migrate_state( - entity_slug=self.kwargs["entity_slug"], - user_model=self.request.user, - itemtxs_qs=itemtxs_qs, - raise_exception=False, - ) - - messages.add_message( - request, - message=f"Items for Invoice {bill_model.bill_number} saved.", - level=messages.SUCCESS, - ) - - # if valid get saved formset from DB - return HttpResponseRedirect( - redirect_to=reverse( - "bill-update", - kwargs={ - "dealer_slug": dealer_slug, - "entity_slug": entity_model.slug, - "bill_pk": bill_pk, - }, - ) - ) - context = self.get_context_data(itemtxs_formset=itemtxs_formset) - return self.render_to_response(context=context) - return super(BillModelUpdateViewView, self).post( - request, dealer_slug, entity_slug, bill_pk, **kwargs - ) - - def get_success_url(self): - return reverse( - "bill-update", - kwargs={ - "dealer_slug": self.kwargs["dealer_slug"], - "entity_slug": self.kwargs["entity_slug"], - "bill_pk": self.kwargs["bill_pk"], - }, - ) - - # @login_required # @permission_required("django_ledger.add_billmodel", raise_exception=True) # def bill_create(request): @@ -9298,7 +9254,7 @@ def add_activity(request,dealer_slug, content_type, slug): @login_required -@permission_required("inventory.add_task", raise_exception=True) +@permission_required("inventory.add_tasks", raise_exception=True) def add_task(request,dealer_slug, content_type, slug): # Get user information for logging user_username = request.user.username if request.user.is_authenticated else 'anonymous' @@ -9329,11 +9285,17 @@ def add_task(request,dealer_slug, content_type, slug): task.created_by = request.user task.due_date = form.cleaned_data["due_date"] task.save() + models.Activity.objects.create( + dealer=dealer, + content_object=obj, + activity_type="task", + created_by=request.user, + notes="Task Added") # --- Log for successful task creation --- logger.info( f"User {user_username} successfully added task " - f"(Assigned to: {task.assigned_to.username}) for {content_type} ID: {obj.slug} " - f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.notes if hasattr(task, 'notes') else 'N/A'}'." + f"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} " + f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'." ) messages.success(request, _("Task added successfully")) else: @@ -9347,7 +9309,7 @@ def add_task(request,dealer_slug, content_type, slug): @login_required -@permission_required("inventory.change_task", raise_exception=True) +@permission_required("inventory.change_tasks", raise_exception=True) def update_task(request,dealer_slug, pk): task = get_object_or_404(models.Tasks, pk=pk) @@ -9361,7 +9323,7 @@ def update_task(request,dealer_slug, pk): @login_required -@permission_required("inventory.add_note", raise_exception=True) +@permission_required("inventory.add_notes", raise_exception=True) def add_note(request,dealer_slug, content_type, slug): # Get user information for logging user_username = request.user.username if request.user.is_authenticated else 'anonymous' @@ -9391,25 +9353,32 @@ def add_note(request,dealer_slug, content_type, slug): note.created_by = request.user note.save() + models.Activity.objects.create( + dealer=dealer, + content_object=obj, + activity_type="note", + created_by=request.user, + notes="Note Added") # --- Single-line log for successful note creation --- logger.info( f"User {user_username} successfully added a note " f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). " - f"Note: '{note.notes[:50]}...'." + f"Note: '{note.note[:50]}...'." ) messages.success(request, _("Note added successfully")) else: # --- Single-line log for invalid form data --- logger.warning( f"User {user_username} submitted invalid note form data " - f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}" + f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_text()}" ) messages.error(request, _("Note form is not valid")) return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug) @login_required -@permission_required("inventory.change_note", raise_exception=True) +@require_http_methods(["POST"]) +@permission_required("inventory.change_notes", raise_exception=True) def update_note(request,dealer_slug, pk): note = get_object_or_404(models.Notes, pk=pk) lead = get_object_or_404(models.Lead, pk=note.content_object.id) @@ -9418,13 +9387,12 @@ def update_note(request,dealer_slug, pk): note.note = request.POST.get("note") note.save() messages.success(request, _("Note updated successfully")) - return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug) - else: - messages.error(request, _("Note form is not valid")) - notes = models.Notes.objects.filter( - content_type__model="lead", object_id=lead.id, dealer=dealer - ) - return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes}) + # else: + # messages.error(request, _("Note form is not valid")) + # notes = models.Notes.objects.filter( + # content_type__model="lead", object_id=lead.pk, dealer=dealer + # ) + return redirect(request.META.get("HTTP_REFERER", "/")) # Admin Management @@ -9841,22 +9809,49 @@ def inventory_items_filter(request, dealer_slug): return render(request, "purchase_orders/car_inventory_item_form.html", context) -class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase): +class PurchaseOrderDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): + slug_url_kwarg = 'po_pk' + slug_field = 'uuid' + context_object_name = 'po_model' + extra_context = { + 'header_subtitle_icon': 'uil:bill', + 'hide_menu': True + } template_name = "purchase_orders/po_detail.html" - context_object_name = "po_model" - permission_required = ["django_ledger.change_purchaseordermodel"] + permission_required = ["django_ledger.view_purchaseordermodel"] def get_queryset(self): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model") return super().get_queryset() - def get_context_data(self, **kwargs): + + def get_context_data(self, *, object_list=None, **kwargs): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) context = super().get_context_data(**kwargs) + # context = super().get_context_data(object_list=object_list, **kwargs) context["entity_slug"] = dealer.entity.slug + po_model: PurchaseOrderModel = self.object + title = f'Purchase Order {po_model.po_number}' + context['page_title'] = title + context['header_title'] = title + + po_model: PurchaseOrderModel = self.object + po_items_qs, item_data = po_model.get_itemtxs_data( + queryset=po_model.itemtransactionmodel_set.all().select_related('item_model', 'bill_model') + ) + context['po_items'] = po_items_qs + context['po_total_amount'] = sum( + i['po_total_amount'] for i in po_items_qs.values( + 'po_total_amount', 'po_item_status') if i['po_item_status'] != 'cancelled') return context + # def get_context_data(self, **kwargs): + # dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) + # context = super().get_context_data(**kwargs) + # context["entity_slug"] = dealer.entity.slug + # return context + class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = PurchaseOrderModel @@ -9943,200 +9938,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie context["entity_slug"] = dealer.entity.slug return context - class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): - template_name = "purchase_orders/po_update.html" - context_object_name = "po_model" - permission_required = ["django_ledger.change_purchaseordermodel"] - - def get_context_data(self, itemtxs_formset=None, **kwargs): - dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - context = super().get_context_data(**kwargs) - context["entity_slug"] = dealer.entity.slug - po_model: PurchaseOrderModel = self.object - if not itemtxs_formset: - itemtxs_qs = self.get_po_itemtxs_qs(po_model) - itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs) - po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) - itemtxs_formset = po_itemtxs_formset_class( - entity_slug=dealer.entity.slug, - user_model=dealer.entity.admin, - po_model=po_model, - queryset=itemtxs_qs, - ) - else: - itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data() - - context["itemtxs_qs"] = itemtxs_qs - context["itemtxs_formset"] = itemtxs_formset - return context - - def get_success_url(self): - return reverse( - "purchase_order_update", - kwargs={ - "dealer_slug": self.kwargs["dealer_slug"], - "entity_slug": self.kwargs["entity_slug"], - "po_pk": self.kwargs["po_pk"], - }, - ) - - def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): - if self.action_update_items: - return HttpResponseRedirect( - redirect_to=reverse( - "purchase_order_update", - kwargs={ - "dealer_slug": dealer_slug, - "entity_slug": entity_slug, - "po_pk": po_pk, - }, - ) - ) - return super(PurchaseOrderUpdateView, self).get( - request, dealer_slug, entity_slug, po_pk, *args, **kwargs - ) - - def post(self, request, dealer_slug, entity_slug, *args, **kwargs): - if self.action_update_items: - if not request.user.is_authenticated: - return HttpResponseForbidden() - queryset = self.get_queryset() - po_model: PurchaseOrderModel = self.get_object(queryset=queryset) - self.object = po_model - po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model) - itemtxs_formset = po_itemtxs_formset_class( - request.POST, - user_model=request.dealer.entity.admin, - po_model=po_model, - entity_slug=entity_slug, - ) - - if itemtxs_formset.has_changed(): - if itemtxs_formset.is_valid(): - itemtxs_list = itemtxs_formset.save(commit=False) - create_bill_uuids = [ - str(i["uuid"].uuid) - for i in itemtxs_formset.cleaned_data - if i and i["create_bill"] is True - ] - - if create_bill_uuids: - item_uuids = ",".join(create_bill_uuids) - redirect_url = reverse( - "bill-create-po", - kwargs={ - "dealer_slug": self.kwargs["dealer_slug"], - "entity_slug": self.kwargs["entity_slug"], - "po_pk": po_model.uuid, - }, - ) - redirect_url += f"?item_uuids={item_uuids}" - return HttpResponseRedirect(redirect_url) - - for itemtxs in itemtxs_list: - if not itemtxs.po_model_id: - itemtxs.po_model_id = po_model.uuid - itemtxs.clean() - - itemtxs_list = itemtxs_formset.save() - po_model.update_state() - po_model.clean() - po_model.save( - update_fields=["po_amount", "po_amount_received", "updated"] - ) - # if valid get saved formset from DB - messages.add_message( - request, messages.SUCCESS, "PO items updated successfully." - ) - return self.render_to_response(context=self.get_context_data()) - # if not valid, return formset with errors... - return self.render_to_response( - context=self.get_context_data(itemtxs_formset=itemtxs_formset) - ) - return super(PurchaseOrderUpdateView, self).post( - request, dealer_slug, entity_slug, *args, **kwargs - ) - - def get_form(self, form_class=None): - po_model: PurchaseOrderModel = self.object - if po_model.is_draft(): - return DraftPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs["entity_slug"], - user_model=self.request.user, - **self.get_form_kwargs(), - ) - elif po_model.is_review(): - return ReviewPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs["entity_slug"], - user_model=self.request.user, - **self.get_form_kwargs(), - ) - elif po_model.is_approved(): - return ApprovedPurchaseOrderModelUpdateForm( - entity_slug=self.kwargs["entity_slug"], - user_model=self.request.user, - **self.get_form_kwargs(), - ) - return BasePurchaseOrderModelUpdateForm( - entity_slug=self.kwargs["entity_slug"], - user_model=self.request.user, - **self.get_form_kwargs(), - ) - + pass class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): - def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs): - return reverse( - "purchase_order_update", - kwargs={ - "dealer_slug": dealer_slug, - "entity_slug": entity_slug, - "po_pk": po_pk, - }, - ) - - def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs): - # kwargs["user_model"] = self.request.user - # Get user information for logging - user_username = request.user.username if request.user.is_authenticated else 'anonymous' - - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - kwargs["user_model"] = dealer.entity.admin - if not self.action_name: - raise ImproperlyConfigured("View attribute action_name is required.") - response = super(BasePurchaseOrderActionActionView, self).get( - request, dealer_slug, entity_slug, po_pk, *args, **kwargs - ) - po_model: PurchaseOrderModel = self.get_object() - - # Log the attempt to perform the action - logger.debug( - f"User {user_username} attempting to call action '{self.action_name}' " - f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." - ) - try: - getattr(po_model, self.action_name)(commit=self.commit, **kwargs) - # --- Single-line log for successful action --- - logger.info( - f"User {user_username} successfully executed action '{self.action_name}' " - f"on Purchase Order ID: {po_model.pk}." - ) - messages.add_message( - request, - message="PO updated successfully.", - level=messages.SUCCESS, - ) - except ValidationError as e: - # --- Single-line log for ValidationError --- - logger.warning( - f"User {user_username} encountered a validation error " - f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " - f"Error: {e}" - ) - print(e) - return response - + permission_required = "django_ledger.change_purchaseordermodel" class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): template_name = "purchase_orders/po_delete.html" @@ -10178,16 +9984,7 @@ class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView): ##############################bil class BaseBillActionView(BaseBillActionViewBase): - def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs): - return reverse( - "bill-update", - kwargs={ - "dealer_slug": self.kwargs["dealer_slug"], - "entity_slug": entity_slug, - "bill_pk": bill_pk, - }, - ) - + pass class BillModelActionMarkAsDraftView(BaseBillActionView): action_name = "mark_as_draft" @@ -10388,6 +10185,9 @@ def upload_cars(request, dealer_slug, pk=None): logger.info( f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}." ) + return redirect( + "inventory_stats",kwargs={"dealer_slug": dealer_slug} + ) # --- Log for successful CSV import and car creation --- logger.info( f"User {user_username} successfully imported {cars_created} cars " diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index bb799424..c70b13d2 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -68,7 +68,7 @@ class="btn btn-sm btn-phoenix-warning me-md-2"> {% trans 'Update' %} - + {% if bill.can_pay %} + + + + + + + \ No newline at end of file diff --git a/templates/components/schedule_modal.html b/templates/components/schedule_modal.html new file mode 100644 index 00000000..1a0b87a6 --- /dev/null +++ b/templates/components/schedule_modal.html @@ -0,0 +1,20 @@ +{% load i18n crispy_forms_filters %} + diff --git a/templates/components/task_modal.html b/templates/components/task_modal.html index 340d1025..f1e7ad8b 100644 --- a/templates/components/task_modal.html +++ b/templates/components/task_modal.html @@ -1,5 +1,11 @@ {% load static i18n crispy_forms_tags %} + -
+

{{ _("Opportunities") }} ({{ lead.get_opportunities.count}})

{% if perms.inventory.add_opportunity%} @@ -301,8 +303,8 @@ {{ _("Note") }} - {{ _("Created By")}} - {{ _("Created On")}} + {{ _("Created On")}} + {{ _("Last Updated")}} @@ -310,12 +312,8 @@ {% for note in notes %} {{note.note}} - {% if note.created_by.staff %} - {{ note.created_by.staff.name }} - {% else %} - {{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }} - {% endif %} {{ note.created|naturalday|capfirst }} + {{ note.updated|naturalday|capfirst }} {% if note.created_by == request.user %} {{email.from_email}} {{email.created}} - Send + Send draft {% endfor %} @@ -469,7 +467,7 @@
{% comment %} {% endcomment %} -
+

{{ _("Tasks") }}

{% if perms.inventory.change_lead%} @@ -533,60 +531,16 @@
{% endcomment %} - - {% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %} - + {% include "components/task_modal.html" with content_type="lead" slug=lead.slug %} - + {% include "components/note_modal.html" with content_type="lead" slug=lead.slug %} + + {% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %} {% endblock content %} {% block customJS %}