add management commands for invoice due date

This commit is contained in:
ismail 2025-08-06 19:35:55 +03:00
parent 65d7e8c685
commit 87dabb97cc
8 changed files with 226 additions and 65 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

@ -11007,11 +11007,9 @@ class RecallDetailView(DetailView):
return context return context
def RecallFilterView(request): def RecallFilterView(request):
context = {'make_data': models.CarMake.objects.all()} context = {'make_data': models.CarMake.objects.all()}
if request.method == "POST": if request.method == "POST":
print(request.POST)
make = request.POST.get('make') make = request.POST.get('make')
model = request.POST.get('model') model = request.POST.get('model')
serie = request.POST.get('serie') 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 %} {% if request.is_dealer %}
<h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6> <h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6>
{% else %} {% 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 %} {% endif %}
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load tenhal_tag %}
{% block title %} {% block title %}
{{ _("Car Sale Report") |capfirst }} {{ _("Car Sale Report") |capfirst }}
{% endblock title %} {% endblock title %}
@ -98,8 +99,8 @@
<div class="col-md-6 col-lg-3"> <div class="col-md-6 col-lg-3">
<div class="card summary-card"> <div class="card summary-card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans 'Total Revenue' %}<i class="fas fa-dollar-sign ms-2"></i></h5> <h5 class="card-title">{% trans 'Total Revenue' %}<span class="icon-saudi_riyal"></span></h5>
<p class="card-text">{{ total_revenue|floatformat:2 }}</p> <p class="card-text">{{ total_revenue|floatformat:2 }} <span class="icon-saudi_riyal"></span></p>
</div> </div>
</div> </div>
</div> </div>
@ -107,7 +108,7 @@
<div class="card summary-card"> <div class="card summary-card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans 'Total VAT Amount' %}<i class="fas fa-percent ms-2"></i></h5> <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> </div>
</div> </div>
@ -115,7 +116,7 @@
<div class="card summary-card"> <div class="card summary-card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans 'Total Discount Amount' %}<i class="fas fa-tag ms-2"></i></h5> <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> </div>
</div> </div>
@ -174,14 +175,14 @@
<td class="fs-9">{{ car.id_car_serie.name }}</td> <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.id_car_trim.name }}</td>
<td class="fs-9">{{ car.mileage }}</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.created_at|date }}</td>
<td class="fs-9">{{ car.invoice.date_paid|date|default_if_none:"-" }}</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.cost_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.marked_price }}</td> <td class="fs-9">{{ car.finances.marked_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.discount_amount }}</td> <td class="fs-9">{{ car.finances.discount_amount }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.selling_price }}</td> <td class="fs-9">{{ car.finances.selling_price }} <span class="icon-saudi_riyal"></span></td>
<td class="fs-9">{{ car.finances.vat_amount }}</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> <td class="fs-9">{{ car.invoice.invoice_number }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -256,6 +256,7 @@
<tr class="bg-body-secondary total-sum"> <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 ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
<td class="align-middle text-start text-danger fw-semibold"> <td class="align-middle text-start text-danger fw-semibold">
{% if estimate.is_draft %}
<form action="{% url 'update_estimate_discount' request.dealer.slug estimate.pk %}" <form action="{% url 'update_estimate_discount' request.dealer.slug estimate.pk %}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
@ -270,6 +271,9 @@
<button type="submit" class="btn btn-sm btn-phoenix-primary ms-n2">{% trans "Update" %}</button> <button type="submit" class="btn btn-sm btn-phoenix-primary ms-n2">{% trans "Update" %}</button>
</div> </div>
</form> </form>
{% else %}
{{ data.total_discount }} <span class="icon-saudi_riyal"></span>
{% endif %}
</td> </td>
</tr> </tr>
<tr class="bg-body-secondary total-sum"> <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> <small><span class="fw-semibold">+ {{ service.name }} - {{ service.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small>
<br> <br>
{% endfor %} {% endfor %}
{% if estimate.is_draft %}
<button class="btn btn-phoenix-primary btn-xs ms-auto" <button class="btn btn-phoenix-primary btn-xs ms-auto"
type="button" type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#additionalModal"> data-bs-target="#additionalModal">
<span class="fas fa-plus me-1"></span>{{ _("Add") }} <span class="fas fa-plus me-1"></span>{{ _("Add") }}
</button> </button>
{% endif %}
</td> </td>
</tr> </tr>
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">

View File

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