diff --git a/inventory/management/commands/invoices_due_date_reminder.py b/inventory/management/commands/invoices_due_date_reminder.py new file mode 100644 index 00000000..d331ed6c --- /dev/null +++ b/inventory/management/commands/invoices_due_date_reminder.py @@ -0,0 +1,134 @@ +import logging +from datetime import timedelta +from django.urls import reverse +from django.conf import settings +from django.utils import timezone +from inventory.tasks import send_email +from django.core.management.base import BaseCommand +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django_ledger.models import InvoiceModel,EstimateModel +from inventory.models import ExtraInfo,Notification,CustomGroup + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = "Handles invoices due date reminders" + + def handle(self, *args, **options): + self.stdout.write("Starting invoices due date reminders..") + + # 1. Send expiration reminders + self.invocie_expiration_reminders() + # self.invoice_past_due() + + # # 2. Deactivate expired plans + # self.deactivate_expired_plans() + + # # 3. Clean up old incomplete orders + # self.cleanup_old_orders() + + # self.stdout.write("Reminders completed!") + + def invocie_expiration_reminders(self): + """Queue email reminders for expiring plans""" + reminder_days = getattr(settings, 'INVOICE_PAST_DUE_REMIND', [3, 7, 14]) + today = timezone.now().date() + + for days in reminder_days: + target_date = today + timedelta(days=days) + expiring_plans = InvoiceModel.objects.filter( + date_due=target_date + ).select_related('customer','ce_model') + + for inv in expiring_plans: + # dealer = inv.customer.customer_set.first().dealer + subject = f"Your invoice is due in {days} days" + message = render_to_string('emails/invoice_past_due_reminder.txt', { + 'customer_name': inv.customer.customer_name, + 'invoice_number': inv.invoice_number, + 'amount_due': inv.amount_due, + 'days_past_due': inv.due_in_days(), + 'SITE_NAME': settings.SITE_NAME + }) + send_email( + 'noreply@yourdomain.com', + inv.customer.email, + subject, + message, + ) + + self.stdout.write(f"Queuing {expiring_plans} reminders for {target_date}") + + def invoice_past_due(self): + """Queue email reminders for expiring plans""" + today = timezone.now().date() + expiring_plans = InvoiceModel.objects.filter( + date_due__lte = today + ).select_related('customer','ce_model') + + # Send email + for inv in expiring_plans: + dealer = inv.customer.customer_set.first().dealer + + subject = f"Your invoice is past due" + message = render_to_string('emails/invoice_past_due.txt', { + 'customer_name': inv.customer.customer_name, + 'invoice_number': inv.invoice_number, + 'amount_due': inv.amount_due, + 'days_past_due': (today - inv.date_due).days, + 'SITE_NAME': settings.SITE_NAME + }) + + # send notification to accountatnt + recipients = ( + CustomGroup.objects.filter(dealer=dealer, name="Accountant") + .first() + .group.user_set.exclude(email=dealer.user.email) + .distinct() + ) + for rec in recipients: + Notification.objects.create( + user=rec, + message=_( + """ + Invoice {invoice_number} is past due,please your + View. + """ + ).format( + invoice_number=inv.invoice_number, + url=reverse( + "invoice_detail", + kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk}, + ), + ), + ) + + # send email to customer + send_email( + 'noreply@yourdomain.com', + inv.customer.email, + subject, + message, + ) + + self.stdout.write(f"Queuing {expiring_plans} reminders for {today}") + + # def deactivate_expired_plans(self): + # """Deactivate plans that have expired (synchronous)""" + # expired_plans = UserPlan.objects.filter( + # active=True, + # expire__lt=timezone.now().date() + # ) + # count = expired_plans.update(active=False) + # self.stdout.write(f"Deactivated {count} expired plans") + + # def cleanup_old_orders(self): + # """Delete incomplete orders older than 30 days""" + # cutoff = timezone.now() - timedelta(days=30) + # count, _ = Order.objects.filter( + # created__lt=cutoff, + # status=Order.STATUS.NEW + # ).delete() + # self.stdout.write(f"Cleaned up {count} old incomplete orders") \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index f87b5c89..fac1f7c5 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -2197,7 +2197,7 @@ 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. @@ -2218,7 +2218,7 @@ class StaffDetailView(LoginRequiredMixin, DetailView): model = models.Staff template_name = "staff/staff_detail.html" context_object_name = "staff" - + def dealer_vat_rate_update(request, slug): @@ -6890,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")) @@ -6966,7 +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) @@ -7018,7 +7018,7 @@ class OpportunityStageUpdateView( form_class = forms.OpportunityStageForm success_message = _("Opportunity Stage updated successfully.") permission_required = ["inventory.change_opportunity"] - + def get_success_url(self): return reverse_lazy( @@ -7051,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) @@ -7144,7 +7144,7 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) staff = self.request.staff queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) - + # Stage filter stage = self.request.GET.get("stage") @@ -7424,7 +7424,7 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) - + class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ Represents a view for creating item expense entries. @@ -7518,7 +7518,7 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV - + class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -9517,7 +9517,7 @@ def payment_callback(request, dealer_slug): 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.") @@ -10400,7 +10400,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug context["vendors"] = vendors - + return context @@ -10881,7 +10881,7 @@ def car_sale_report_view(request, dealer_slug): 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 @@ -10921,22 +10921,22 @@ def car_sale_report_csv_export(request,dealer_slug): writer = csv.writer(response) header=[ - 'Make', - 'VIN', - 'Model', - 'Year', - 'Serie', - 'Trim', - 'Mileage', - 'Stock Type', - 'Created Date', - 'Sold Date', - 'Cost Price', - 'Marked Price', - 'Discount Amount', - 'Selling Price', - 'Tax Amount', - 'Invoice Number', + 'Make', + 'VIN', + 'Model', + 'Year', + 'Serie', + 'Trim', + 'Mileage', + 'Stock Type', + 'Created Date', + 'Sold Date', + 'Cost Price', + 'Marked Price', + 'Discount Amount', + 'Selling Price', + 'Tax Amount', + 'Invoice Number', ] writer.writerow(header) @@ -11007,11 +11007,9 @@ class RecallDetailView(DetailView): return context - def RecallFilterView(request): context = {'make_data': models.CarMake.objects.all()} if request.method == "POST": - print(request.POST) make = request.POST.get('make') model = request.POST.get('model') serie = request.POST.get('serie') diff --git a/templates/emails/invoice_past_due.txt b/templates/emails/invoice_past_due.txt new file mode 100644 index 00000000..fe38285a --- /dev/null +++ b/templates/emails/invoice_past_due.txt @@ -0,0 +1,10 @@ +Hello {{ customer_name }}, + +This is a friendly reminder that your invoice for {{ invoice_number }} is now {{ days_past_due }} days past due. + +Please settle your outstanding balance of {{ amount_due }} . + +If you have already paid, please disregard this notice. + +Best regards, +{{ SITE_NAME }} Team diff --git a/templates/emails/invoice_past_due_reminder.txt b/templates/emails/invoice_past_due_reminder.txt new file mode 100644 index 00000000..fc1710cc --- /dev/null +++ b/templates/emails/invoice_past_due_reminder.txt @@ -0,0 +1,11 @@ +Hello {{ customer_name }}, + +This is a friendly reminder that your invoice for {{ invoice_number }} is due in {{ days_past_due }} days. + +Please settle your outstanding balance of {{ amount_due }} before the due date to avoid any late fees. + +If you have already paid, please disregard this notice. + +Best regards, +{{ SITE_NAME }} Team + diff --git a/templates/header.html b/templates/header.html index e63d64d2..ce1e99c2 100644 --- a/templates/header.html +++ b/templates/header.html @@ -492,7 +492,7 @@ {% if request.is_dealer %}
{{ total_revenue|floatformat:2 }}
+{{ total_revenue|floatformat:2 }}
{{ 10000|floatformat:2 }}
+{{ 10000|floatformat:2 }}
{{ total_discount|floatformat:2 }}
+{{ total_discount|floatformat:2 }}
- {% trans "Name" %}: {{ sale_order.full_name }}
+ {% trans "Name" %}: {{ sale_order.full_name|title }}
{% if sale_order.customer %}
{% trans "Contact" %}: {{ sale_order.customer.phone_number }}
@@ -29,7 +30,7 @@
{% trans "Order Date" %}: {{ sale_order.order_date|date }}
@@ -187,7 +188,7 @@
{% trans "Amount Paid" %}: