This commit is contained in:
Faheedkhan 2025-08-06 19:43:01 +03:00
commit cb01a7f5c5
8 changed files with 227 additions and 66 deletions

View File

@ -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
<a href="{url}" target="_blank">View</a>.
"""
).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")

View File

@ -2197,8 +2197,8 @@ class DealerUpdateView(
def get_success_url(self):
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
class StaffDetailView(LoginRequiredMixin,DetailView):
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):
@ -6895,7 +6895,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"))
@ -6971,7 +6971,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)
@ -7023,7 +7023,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(
@ -7056,7 +7056,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)
@ -7149,7 +7149,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")
@ -7429,7 +7429,7 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
"""
Represents a view for creating item expense entries.
@ -7523,7 +7523,7 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Handles the display of a list of item expenses.
@ -9522,7 +9522,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.")
@ -10405,7 +10405,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
context["vendors"] = vendors
return context
@ -10886,7 +10886,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
@ -10926,22 +10926,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)
@ -11012,11 +11012,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')

View File

@ -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

View File

@ -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

View File

@ -492,7 +492,7 @@
{% if request.is_dealer %}
<h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6>
{% else %}
<h6 class="mt-2 text-body-emphasis">{{ user.staffmember.staff.get_local_name }}</h6>
<h6 class="mt-2 text-body-emphasis">{{ user.staff.get_local_name }}</h6>
{% endif %}
</div>
</div>

View File

@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load tenhal_tag %}
{% block title %}
{{ _("Car Sale Report") |capfirst }}
{% endblock title %}
@ -8,7 +9,7 @@
{% block content%}
<style>
.summary-card {
border: 1px solid var(--card-border);
border-radius: .5rem;
@ -34,9 +35,9 @@
.summary-card .card-text {
font-size: 2.25rem;
font-weight: 700;
}
</style>
<div class="container-fluid report-container">
@ -98,8 +99,8 @@
<div class="col-md-6 col-lg-3">
<div class="card summary-card">
<div class="card-body">
<h5 class="card-title">{% trans 'Total Revenue' %}<i class="fas fa-dollar-sign ms-2"></i></h5>
<p class="card-text">{{ total_revenue|floatformat:2 }}</p>
<h5 class="card-title">{% trans 'Total Revenue' %}<span class="icon-saudi_riyal"></span></h5>
<p class="card-text">{{ total_revenue|floatformat:2 }} <span class="icon-saudi_riyal"></span></p>
</div>
</div>
</div>
@ -107,7 +108,7 @@
<div class="card summary-card">
<div class="card-body">
<h5 class="card-title">{% trans 'Total VAT Amount' %}<i class="fas fa-percent ms-2"></i></h5>
<p class="card-text">{{ 10000|floatformat:2 }}</p>
<p class="card-text">{{ 10000|floatformat:2 }} <span class="icon-saudi_riyal"></span></p>
</div>
</div>
</div>
@ -115,7 +116,7 @@
<div class="card summary-card">
<div class="card-body">
<h5 class="card-title">{% trans 'Total Discount Amount' %}<i class="fas fa-tag ms-2"></i></h5>
<p class="card-text">{{ total_discount|floatformat:2 }}</p>
<p class="card-text">{{ total_discount|floatformat:2 }} <span class="icon-saudi_riyal"></span></p>
</div>
</div>
</div>
@ -174,14 +175,14 @@
<td class="fs-9">{{ car.id_car_serie.name }}</td>
<td class="fs-9">{{ car.id_car_trim.name }}</td>
<td class="fs-9">{{ car.mileage }}</td>
<td class="fs-9">{{ car.stock_type }}</td>
<td class="fs-9">{{ car.stock_type|capfirst }}</td>
<td class="fs-9">{{ car.created_at|date }}</td>
<td class="fs-9">{{ car.invoice.date_paid|date|default_if_none:"-" }}</td>
<td class="fs-9">{{ car.finances.cost_price }}</td>
<td class="fs-9">{{ car.finances.marked_price }}</td>
<td class="fs-9">{{ car.finances.discount_amount }}</td>
<td class="fs-9">{{ car.finances.selling_price }}</td>
<td class="fs-9">{{ car.finances.vat_amount }}</td>
<td class="fs-9">{{ car.finances.cost_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.marked_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.discount_amount }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.selling_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.vat_amount }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.invoice.invoice_number }}</td>
</tr>
{% endfor %}

View File

@ -252,13 +252,14 @@
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
</tr>
{% endfor %}
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
<td class="align-middle text-start text-danger fw-semibold">
<form action="{% url 'update_estimate_discount' request.dealer.slug estimate.pk %}"
method="post">
{% csrf_token %}
{% if estimate.is_draft %}
<form action="{% url 'update_estimate_discount' request.dealer.slug estimate.pk %}"
method="post">
{% csrf_token %}
<div class="input-group input-group-sm">
<input type="number"
class="form-control"
@ -266,10 +267,13 @@
value="{{ data.total_discount }}"
step="0.01"
style="width: 1px">
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
<button type="submit" class="btn btn-sm btn-phoenix-primary ms-n2">{% trans "Update" %}</button>
</div>
</form>
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
<button type="submit" class="btn btn-sm btn-phoenix-primary ms-n2">{% trans "Update" %}</button>
</div>
</form>
{% else %}
{{ data.total_discount }} <span class="icon-saudi_riyal"></span>
{% endif %}
</td>
</tr>
<tr class="bg-body-secondary total-sum">
@ -285,12 +289,14 @@
<small><span class="fw-semibold">+ {{ service.name }} - {{ service.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small>
<br>
{% endfor %}
{% if estimate.is_draft %}
<button class="btn btn-phoenix-primary btn-xs ms-auto"
type="button"
data-bs-toggle="modal"
data-bs-target="#additionalModal">
<span class="fas fa-plus me-1"></span>{{ _("Add") }}
</button>
type="button"
data-bs-toggle="modal"
data-bs-target="#additionalModal">
<span class="fas fa-plus me-1"></span>{{ _("Add") }}
</button>
{% endif %}
</td>
</tr>
<tr class="bg-body-secondary total-sum">
@ -345,7 +351,7 @@ document.addEventListener('htmx:afterSwap', initEstimateFunctions);
function initEstimateFunctions() {
// Initialize calculateTotals if estimate table exists
// Initialize form action setter if form exists
const confirmForm = document.getElementById('confirmForm');

View File

@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% load custom_filters %}
{% load tenhal_tag %}
{% block title %}{{ page_title }} - {{ sale_order.formatted_order_id }}{% endblock %}
{% block content %}
<div class="container mt-4">
@ -17,9 +18,9 @@
<!-- Basic Information -->
<div class="row mb-4">
<div class="col-md-4">
<h4>{% trans "Customer Information" %}</h4>
<h4 class="mb-3">{% trans "Customer Information" %}</h4>
<p>
<strong>{% trans "Name" %}:</strong> {{ sale_order.full_name }}
<strong>{% trans "Name" %}:</strong> {{ sale_order.full_name|title }}
<br>
{% if sale_order.customer %}
<strong>{% trans "Contact" %}:</strong> {{ sale_order.customer.phone_number }}
@ -29,7 +30,7 @@
</p>
</div>
<div class="col-md-4">
<h4>{% trans "Order Details" %}</h4>
<h4 class="mb-3">{% trans "Order Details" %}</h4>
<p>
<strong>{% trans "Order Date" %}:</strong> {{ sale_order.order_date|date }}
<br>
@ -187,7 +188,7 @@
<strong>{% trans "Amount Paid" %}:</strong>
</td>
<td style="padding-left: 0.5rem;">
<span class="currency">{{ CURRENCY }}</span>{{ sale_order.invoice.amount_paid|floatformat:2 }}
<span class="icon-saudi_riyal"></span>{{ sale_order.invoice.amount_paid|floatformat:2 }}
</td>
</tr>
<tr>
@ -195,7 +196,7 @@
<strong>{% trans "Balance Due" %}:</strong>
</td>
<td style="padding-left: 0.5rem;">
<span class="currency">{{ CURRENCY }}</span>{{ sale_order.invoice.amount_due|floatformat:2 }}
<span class="icon-saudi_riyal"></span>{{ sale_order.invoice.amount_due|floatformat:2 }}
</td>
</tr>
<tr>
@ -203,7 +204,7 @@
<strong>{% trans "Amount Unearned" %}:</strong>
</td>
<td style="padding-left: 0.5rem;">
<span class="currency">{{ CURRENCY }}</span>{{ sale_order.invoice.amount_unearned|floatformat:2 }}
<span class="icon-saudi_riyal"></span>{{ sale_order.invoice.amount_unearned|floatformat:2 }}
</td>
</tr>
<tr>
@ -211,7 +212,7 @@
<strong>{% trans "Amount Receivable" %}:</strong>
</td>
<td style="padding-left: 0.5rem;">
<span class="currency">{{ CURRENCY }}</span>{{ sale_order.invoice.amount_receivable|floatformat:2 }}
<span class="icon-saudi_riyal"></span>{{ sale_order.invoice.amount_receivable|floatformat:2 }}
</td>
</tr>
</tbody>