1474 lines
58 KiB
Python
1474 lines
58 KiB
Python
import os
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from django.urls import reverse
|
|
from django.contrib.auth.models import Group
|
|
from django.db.models.signals import post_save, post_delete
|
|
from django.dispatch import receiver
|
|
|
|
# from appointment.models import Service
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.auth import get_user_model
|
|
from django_ledger.io import roles
|
|
from django_ledger.models import (
|
|
EntityModel,
|
|
ItemModel,
|
|
JournalEntryModel,
|
|
TransactionModel,
|
|
LedgerModel,
|
|
AccountModel,
|
|
PurchaseOrderModel,
|
|
EstimateModel,
|
|
BillModel,
|
|
ChartOfAccountModel,
|
|
CustomerModel,
|
|
)
|
|
from . import models
|
|
from django.utils.timezone import now
|
|
from django.db import transaction
|
|
from django_q.tasks import async_task
|
|
from plans.models import UserPlan
|
|
from plans.signals import order_completed, activate_user_plan
|
|
from inventory.tasks import send_email
|
|
from django.conf import settings
|
|
|
|
# logging
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
User = get_user_model()
|
|
|
|
# @receiver(post_save, sender=models.SaleQuotation)
|
|
# def link_quotation_to_entity(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# # Get the corresponding Django Ledger entity for the dealer
|
|
# entity = EntityModel.objects.get(name=instance.dealer.get_root_dealer.name)
|
|
# instance.entity = entity
|
|
# instance.save()
|
|
# @receiver(pre_delete, sender=models.Dealer)
|
|
# def remove_user_account(sender, instance, **kwargs):
|
|
# user = instance.user
|
|
# if user:
|
|
# user.delete()
|
|
#
|
|
# @receiver(post_save, sender=User)
|
|
# def create_dealer(instance, created, *args, **kwargs):
|
|
# if created:
|
|
# if not instance.dealer:
|
|
#
|
|
# models.Dealer.objects.create(user=instance,name=instance.username,email=instance.email)
|
|
#
|
|
# @receiver(post_save, sender=models.Dealer)
|
|
# def create_user_account(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# if instance.dealer_type != "Owner":
|
|
# user = User.objects.create_user(
|
|
# username=instance.name,
|
|
# email=instance.email,
|
|
# )
|
|
# user.set_password("Tenhal@123")
|
|
# user.save()
|
|
# instance.user = user
|
|
# instance.save()
|
|
|
|
|
|
@receiver(post_save, sender=models.Car)
|
|
def create_dealers_make(sender, instance, created, **kwargs):
|
|
if created:
|
|
models.DealersMake.objects.get_or_create(
|
|
dealer=instance.dealer, car_make=instance.id_car_make
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=models.Car)
|
|
def create_car_location(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal handler to create a CarLocation entry when a new Car instance is created.
|
|
The function ensures that the associated Car has a dealer before creating its
|
|
CarLocation. If the dealer is missing, a ValueError is raised, and an error
|
|
message is logged.
|
|
|
|
:param sender: The model class that sends the signal, typically `models.Car`.
|
|
:type sender: Type[models.Model]
|
|
:param instance: The actual instance of the Car model that triggered the signal.
|
|
:param created: A boolean value indicating whether a new record was created.
|
|
:type created: bool
|
|
:param kwargs: Additional keyword arguments provided with the signal.
|
|
:type kwargs: dict
|
|
:return: None
|
|
"""
|
|
try:
|
|
if created:
|
|
# Log that the signal was triggered for a new car
|
|
logger.debug(
|
|
f"Post-save signal triggered for new Car (VIN: {instance.vin}). Attempting to create CarLocation."
|
|
)
|
|
|
|
if instance.dealer is None:
|
|
# Log the critical data integrity error before raising
|
|
logger.error(
|
|
f"Attempted to create CarLocation for car (VIN: {instance.vin}) "
|
|
f"but 'dealer' field is missing. This indicates a data integrity issue or unhandled logic."
|
|
)
|
|
raise ValueError(
|
|
f"Cannot create CarLocation for car {instance.vin}: dealer is missing."
|
|
)
|
|
|
|
models.CarLocation.objects.create(
|
|
car=instance,
|
|
owner=instance.dealer,
|
|
showroom=instance.dealer,
|
|
description=f"Initial location set for car {instance.vin}.",
|
|
)
|
|
# Log successful CarLocation creation
|
|
logger.info(
|
|
f"Successfully created CarLocation for new car (VIN: {instance.vin}) "
|
|
f"with owner '{instance.dealer.name}' (ID: {instance.dealer.pk})."
|
|
)
|
|
except Exception as e:
|
|
# --- Single-line log for general error during CarLocation creation ---
|
|
logger.error(
|
|
f"Failed to create CarLocation for car (VIN: {instance.vin}). "
|
|
f"An unexpected error occurred: {e}",
|
|
exc_info=True,
|
|
)
|
|
print(f"Failed to create CarLocation for car {instance.vin}: {e}")
|
|
|
|
|
|
@receiver(post_save, sender=models.Dealer)
|
|
def create_ledger_entity(sender, instance, created, **kwargs):
|
|
if not created:
|
|
return
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
# Create entity
|
|
entity = models.EntityModel.create_entity(
|
|
name=instance.user.dealer.name,
|
|
admin=instance.user,
|
|
use_accrual_method=True,
|
|
fy_start_month=1,
|
|
)
|
|
if not entity:
|
|
raise Exception("Entity creation failed")
|
|
|
|
instance.entity = entity
|
|
instance.save(update_fields=["entity"])
|
|
|
|
# Create default COA
|
|
entity.create_chart_of_accounts(
|
|
assign_as_default=True, commit=True, coa_name=f"{entity.name}-COA"
|
|
)
|
|
|
|
logger.info(
|
|
f"✅ Setup complete for dealer {instance.id}: entity & COA ready."
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"💥 Failed setup for dealer {instance.id}: {e}")
|
|
# Optional: schedule retry or alert
|
|
|
|
|
|
# Create Entity
|
|
# @receiver(post_save, sender=models.Dealer)
|
|
# def create_ledger_entity(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal handler for creating ledger entities and initializing accounts for a new Dealer instance upon creation.
|
|
|
|
# This signal is triggered when a new Dealer instance is saved to the database. It performs the following actions:
|
|
# 1. Creates a ledger entity for the Dealer with necessary configurations.
|
|
# 2. Generates a chart of accounts (COA) for the entity and assigns it as the default.
|
|
# 3. Creates predefined unit of measures (UOMs) related to the entity.
|
|
# 4. Initializes and assigns default accounts under various roles (e.g., assets, liabilities) for the entity with their
|
|
# respective configurations (e.g., account code, balance type).
|
|
|
|
# This function ensures all necessary financial records and accounts are set up when a new Dealer is added, preparing the
|
|
# system for future financial transactions and accounting operations.
|
|
|
|
# :param sender: The model class that sent the signal (in this case, Dealer).
|
|
# :param instance: The instance of the model being saved.
|
|
# :param created: A boolean indicating whether a new record was created.
|
|
# :param kwargs: Additional keyword arguments passed by the signal.
|
|
# :return: None
|
|
# """
|
|
# if created:
|
|
# entity_name = instance.user.dealer.name
|
|
# entity = EntityModel.create_entity(
|
|
# name=entity_name,
|
|
# admin=instance.user,
|
|
# use_accrual_method=True,
|
|
# fy_start_month=1,
|
|
# )
|
|
|
|
# if entity:
|
|
# instance.entity = entity
|
|
# instance.save()
|
|
# coa = entity.create_chart_of_accounts(
|
|
# assign_as_default=True, commit=True, coa_name=_(f"{entity_name}-COA")
|
|
# )
|
|
# if coa:
|
|
# # Create unit of measures
|
|
# entity.create_uom(name="Unit", unit_abbr="unit")
|
|
# for u in models.UnitOfMeasure.choices:
|
|
# entity.create_uom(name=u[1], unit_abbr=u[0])
|
|
|
|
# # Create COA accounts, background task
|
|
# async_task(
|
|
# func="inventory.tasks.create_coa_accounts",
|
|
# dealer=instance,
|
|
# hook="inventory.hooks.check_create_coa_accounts",
|
|
# )
|
|
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
|
|
|
|
# create_settings(instance.pk)
|
|
# create_accounts_for_make(instance.pk)
|
|
|
|
|
|
@receiver(post_save, sender=models.Dealer)
|
|
def create_dealer_groups(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal handler to create and assign default groups to a Dealer instance when it
|
|
is created. The groups are based on predefined names and assigned specific
|
|
permissions. Uses transaction hooks to ensure groups are created only after
|
|
successful database commit.
|
|
|
|
:param sender: The model class that triggered the signal.
|
|
:type sender: Type[models.Model]
|
|
:param instance: The instance of the model class that caused the signal to fire.
|
|
:param created: Boolean indicating whether an instance was newly created.
|
|
:param kwargs: Additional keyword arguments passed by the signal.
|
|
:type kwargs: dict
|
|
"""
|
|
if created:
|
|
# async_task("inventory.tasks.create_groups",instance.slug)
|
|
def create_groups():
|
|
for group_name in ["Inventory", "Accountant", "Sales", "Manager"]:
|
|
group = Group.objects.create(name=f"{instance.slug}_{group_name}")
|
|
group_manager = models.CustomGroup.objects.create(
|
|
name=group_name, dealer=instance, group=group
|
|
)
|
|
group_manager.set_default_permissions()
|
|
instance.user.groups.add(group)
|
|
|
|
transaction.on_commit(create_groups)
|
|
|
|
|
|
# Create Vendor
|
|
@receiver(post_save, sender=models.Vendor)
|
|
def create_ledger_vendor(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal function that listens for the `post_save` event on the Vendor model.
|
|
This function is triggered after a Vendor instance is saved. If the instance
|
|
is newly created (`created` is True), it will create necessary related entities
|
|
such as a Vendor model in the corresponding Entity and an Account for that Vendor.
|
|
|
|
:param sender: The model class that triggered the signal event, which is `models.Vendor`.
|
|
:param instance: The specific instance of the `models.Vendor` that was saved.
|
|
:param created: A boolean indicating whether the instance is newly created (`True`)
|
|
or updated (`False`).
|
|
:param kwargs: Additional keyword arguments passed by the signal dispatcher.
|
|
:return: None
|
|
"""
|
|
if created:
|
|
instance.create_vendor_model()
|
|
instance.create_vendor_account(roles.LIABILITY_CL_ACC_PAYABLE)
|
|
else:
|
|
instance.update_vendor_model()
|
|
|
|
|
|
# Create Item
|
|
@receiver(post_save, sender=models.Car)
|
|
def create_item_model(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal handler that triggers upon saving a `Car` model instance. This function is responsible
|
|
for creating or updating an associated product in the related entity's inventory system. The
|
|
new product is created only if it does not already exist, and additional information about the
|
|
car is added to the product's metadata.
|
|
|
|
:param sender: Signal sender, typically the model class triggering the save event.
|
|
:type sender: type
|
|
:param instance: Instance of the `Car` model that was saved.
|
|
:type instance: models.Car
|
|
:param created: Flag indicating whether the model instance was newly created.
|
|
:type created: bool
|
|
:param kwargs: Additional keyword arguments passed by the signal mechanism.
|
|
:type kwargs: dict
|
|
:return: None
|
|
"""
|
|
entity = instance.dealer.entity
|
|
|
|
coa = entity.get_default_coa()
|
|
uom = entity.get_uom_all().filter(name="Unit").first()
|
|
if not uom:
|
|
uom = entity.create_uom(name="Unit", unit_abbr="unit")
|
|
|
|
if not instance.item_model:
|
|
inventory = entity.create_item_product(
|
|
name=instance.vin,
|
|
item_type=ItemModel.ITEM_TYPE_MATERIAL,
|
|
uom_model=uom,
|
|
coa_model=coa,
|
|
)
|
|
instance.item_model = inventory
|
|
instance.save()
|
|
|
|
if instance.cost_price:
|
|
instance.item_model.default_amount = instance.cost_price
|
|
instance.item_model.save()
|
|
|
|
# inventory = entity.create_item_inventory(
|
|
# name=instance.vin,
|
|
# uom_model=uom,
|
|
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
|
|
# )
|
|
# inventory.additional_info = {}
|
|
# inventory.additional_info.update({"car_info": instance.to_dict()})
|
|
# inventory.save()
|
|
# else:
|
|
# instance.item_model.additional_info.update({"car_info": instance.to_dict()})
|
|
# instance.item_model.save()
|
|
|
|
|
|
# # update price - CarFinance
|
|
# @receiver(post_save, sender=models.CarFinance)
|
|
# def update_item_model_cost(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal handler for updating an inventory item's cost and additional information
|
|
# when a CarFinance instance is saved. This function updates the corresponding
|
|
# inventory item of the car dealer's entity associated with the car's VIN by
|
|
# modifying its default amount and updating additional data fields.
|
|
|
|
# :param sender: The model class that triggered the signal.
|
|
# :param instance: The instance of the CarFinance that was saved.
|
|
# :param created: A boolean indicating whether the model instance was newly created.
|
|
# :param kwargs: Additional keyword arguments passed during the signal invocation.
|
|
# :return: None
|
|
# """
|
|
# # if created and not instance.is_sold:
|
|
# # if created:
|
|
# # entity = instance.car.dealer.entity
|
|
# # coa = entity.get_default_coa()
|
|
# # inventory_account = (
|
|
# # entity.get_all_accounts()
|
|
# # .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
|
# # .first()
|
|
# # )
|
|
# # if not inventory_account:
|
|
# # inventory_account = create_make_accounts(
|
|
# # entity,
|
|
# # coa,
|
|
# # [instance.car.id_car_make],
|
|
# # "Inventory",
|
|
# # roles.ASSET_CA_INVENTORY,
|
|
# # "debit",
|
|
# # )
|
|
|
|
# # cogs = (
|
|
# # entity.get_all_accounts()
|
|
# # .filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
|
# # .first()
|
|
# # )
|
|
# # if not cogs:
|
|
# # cogs = create_make_accounts(
|
|
# # entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
|
# # )
|
|
# # revenue = (
|
|
# # entity.get_all_accounts()
|
|
# # .filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
|
# # .first()
|
|
# # )
|
|
# # if not revenue:
|
|
# # revenue = create_make_accounts(
|
|
# # entity,
|
|
# # coa,
|
|
# # [instance.car.id_car_make],
|
|
# # "Revenue",
|
|
# # roles.ASSET_CA_RECEIVABLES,
|
|
# # "credit",
|
|
# # )
|
|
|
|
# # cash_account = (
|
|
# # # entity.get_all_accounts()
|
|
# # # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
|
# # # .first()
|
|
# # entity.get_all_accounts()
|
|
# # .filter(role=roles.ASSET_CA_CASH, role_default=True)
|
|
# # .first()
|
|
# # )
|
|
|
|
# # ledger = LedgerModel.objects.create(
|
|
# # entity=entity, name=f"Inventory Purchase - {instance.car}"
|
|
# # )
|
|
# # je = JournalEntryModel.objects.create(
|
|
# # ledger=ledger,
|
|
# # description=f"Acquired {instance.car} for inventory",
|
|
# # )
|
|
# # TransactionModel.objects.create(
|
|
# # journal_entry=je,
|
|
# # account=inventory_account,
|
|
# # amount=Decimal(instance.cost_price),
|
|
# # tx_type="debit",
|
|
# # description="",
|
|
# # )
|
|
|
|
# # TransactionModel.objects.create(
|
|
# # journal_entry=je,
|
|
# # account=cash_account,
|
|
# # amount=Decimal(instance.cost_price),
|
|
# # tx_type="credit",
|
|
# # description="",
|
|
# # )
|
|
|
|
# instance.car.item_model.default_amount = instance.marked_price
|
|
# # if not isinstance(instance.car.item_model.additional_info, dict):
|
|
# # instance.car.item_model.additional_info = {}
|
|
# # instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
|
|
# # instance.car.item_model.additional_info.update(
|
|
# # {
|
|
# # "additional_services": [
|
|
# # service.to_dict() for service in instance.additional_services.all()
|
|
# # ]
|
|
# # }
|
|
# # )
|
|
# instance.car.item_model.save()
|
|
# print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|
|
|
|
|
|
# @receiver(pre_save, sender=models.SaleQuotation)
|
|
# def update_quotation_status(sender, instance, **kwargs):
|
|
# if instance.valid_until and timezone.now() > instance.valid_until:
|
|
# instance.status = 'expired'
|
|
# # instance.total_price = instance.calculate_total_price()
|
|
#
|
|
#
|
|
# @receiver(post_save, sender=models.Payment)
|
|
# def update_status_on_payment(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# quotation = instance.sale_quotation
|
|
# total_payments = sum(payment.amount for payment in quotation.payments.all())
|
|
# if total_payments >= quotation.amount:
|
|
# quotation.status = 'completed'
|
|
# # SalesInvoice.objects.create(sales_order=order)
|
|
# elif total_payments > 0:
|
|
# quotation.status = 'partially_paid'
|
|
# else:
|
|
# quotation.status = 'pending'
|
|
# quotation.save()
|
|
|
|
|
|
# @receiver(post_save, sender=models.CarColors)
|
|
# def update_car_hash_when_color_changed(sender, instance, **kwargs):
|
|
# """
|
|
# Signal receiver to handle updates to a car instance when its related
|
|
# CarColors instance is modified. Triggered by the `post_save` signal
|
|
# for the `CarColors` model. Ensures that the associated `Car` instance
|
|
# is saved, propagating changes effectively.
|
|
|
|
# :param sender: The model class (`CarColors`) that was saved.
|
|
# :type sender: Type[models.CarColors]
|
|
# :param instance: The specific instance of `CarColors` that was saved.
|
|
# :type instance: models.CarColors
|
|
# :param kwargs: Additional keyword arguments passed by the signal.
|
|
# :type kwargs: dict
|
|
# :return: None
|
|
# """
|
|
|
|
# car = instance.car
|
|
# car.hash = car.get_hash
|
|
# car.save()
|
|
|
|
|
|
@receiver(post_save, sender=models.Opportunity)
|
|
def notify_staff_on_deal_stage_change(sender, instance, **kwargs):
|
|
"""
|
|
Notify staff members when the stage of an Opportunity is updated. This function listens to the `post_save`
|
|
signal for the Opportunity model and triggers a notification if the stage attribute of the Opportunity
|
|
instance has been changed.
|
|
|
|
:param sender: The model class that sends the signal.
|
|
:type sender: type[models.Opportunity]
|
|
:param instance: The actual instance being saved in the Django ORM.
|
|
:type instance: models.Opportunity
|
|
:param kwargs: Additional keyword arguments passed by the signal.
|
|
:type kwargs: dict
|
|
:return: None
|
|
"""
|
|
if instance.pk:
|
|
previous = models.Opportunity.objects.get(pk=instance.pk)
|
|
if previous.stage != instance.stage:
|
|
message = f"Opportunity '{instance.pk}' status changed from {previous.stage} to {instance.stage}."
|
|
models.Notification.objects.create(
|
|
staff=instance.created_by, message=message
|
|
)
|
|
|
|
|
|
# @receiver(post_save, sender=models.Opportunity)
|
|
# def log_opportunity_creation(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# models.OpportunityLog.objects.create(
|
|
# opportunity=instance,
|
|
# action="create",
|
|
# user=instance.created_by,
|
|
# details=f"Opportunity '{instance.deal_name}' was created.",
|
|
# )
|
|
|
|
|
|
# @receiver(pre_save, sender=models.Opportunity)
|
|
# def log_opportunity_update(sender, instance, **kwargs):
|
|
# if instance.pk:
|
|
# previous = models.Opportunity.objects.get(pk=instance.pk)
|
|
# if previous.stage != instance.deal_status:
|
|
# models.OpportunityLog.objects.create(
|
|
# opportunity=instance,
|
|
# action="status_change",
|
|
# user=instance.created_by,
|
|
# old_status=previous.deal_status,
|
|
# new_status=instance.deal_status,
|
|
# details=f"Status changed from {previous.deal_status} to {instance.deal_status}.",
|
|
# )
|
|
# else:
|
|
# models.OpportunityLog.objects.create(
|
|
# opportunity=instance,
|
|
# action="update",
|
|
# user=instance.created_by,
|
|
# details=f"Opportunity '{instance.deal_name}' was updated.",
|
|
# )
|
|
|
|
|
|
@receiver(post_save, sender=models.AdditionalServices)
|
|
def create_item_service(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal handler for creating a service item in the ItemModel when a new
|
|
AdditionalServices instance is created. This function listens to the
|
|
post_save signal of the AdditionalServices model and sets up the related
|
|
ItemModel instance to represent the service.
|
|
|
|
:param sender: The model class that sent the signal.
|
|
:param instance: The instance of the model being saved.
|
|
:param created: Boolean indicating whether a new instance was created.
|
|
:param kwargs: Additional keyword arguments provided by the signal.
|
|
:return: None
|
|
"""
|
|
if created:
|
|
entity = instance.dealer.entity
|
|
uom = entity.get_uom_all().filter(unit_abbr=instance.uom).first()
|
|
cogs = (
|
|
entity.get_all_accounts()
|
|
.filter(role=roles.COGS, active=True, role_default=True)
|
|
.first()
|
|
)
|
|
|
|
service_model = ItemModel.objects.create(
|
|
name=instance.name,
|
|
uom=uom,
|
|
default_amount=instance.price,
|
|
entity=entity,
|
|
is_product_or_service=True,
|
|
sold_as_unit=True,
|
|
is_active=True,
|
|
item_role="service",
|
|
for_inventory=False,
|
|
cogs_account=cogs,
|
|
)
|
|
instance.item = service_model
|
|
instance.save()
|
|
|
|
|
|
@receiver(post_save, sender=models.Lead)
|
|
def track_lead_status_change(sender, instance, **kwargs):
|
|
"""
|
|
Tracks changes in the status of a Lead instance and logs the transition into the
|
|
LeadStatusHistory model. This function is triggered after the `post_save` signal
|
|
is emitted for a Lead instance.
|
|
|
|
The function compares the `status` of the updated Lead instance with its previous
|
|
value. If the `status` has changed, it creates a new entry in the LeadStatusHistory
|
|
model, recording the old status, the new status, and the staff responsible for the change.
|
|
|
|
:param sender: The model class that sent the signal.
|
|
:type sender: Type[models.Model]
|
|
:param instance: The actual instance being saved. It represents the Lead instance
|
|
whose status is being tracked.
|
|
:type instance: models.Lead
|
|
:param kwargs: Additional keyword arguments passed by the signal. These can include
|
|
flags such as 'created' to indicate if the instance was newly created or updated.
|
|
|
|
:return: None
|
|
"""
|
|
if instance.pk: # Ensure the instance is being updated, not created
|
|
# Log that a lead update is being checked for status changes
|
|
logger.debug(f"Checking for status change on Lead ID: {instance.pk}.")
|
|
try:
|
|
old_lead = models.Lead.objects.get(pk=instance.pk)
|
|
if old_lead.status != instance.status: # Check if status has changed
|
|
models.LeadStatusHistory.objects.create(
|
|
lead=instance,
|
|
old_status=old_lead.status,
|
|
new_status=instance.status,
|
|
changed_by=instance.staff, # Assuming the assigned staff made the change
|
|
)
|
|
# --- Single-line log for successful status change and history creation ---
|
|
logger.info(
|
|
f"Lead ID: {instance.pk} status changed from '{old_lead.status}' to '{instance.status}'. "
|
|
f"LeadStatusHistory recorded by Staff: {instance.staff.username if instance.staff else 'N/A'}."
|
|
)
|
|
except models.Lead.DoesNotExist:
|
|
# --- Single-line log for expected Lead.DoesNotExist (e.g., during initial object creation) ---
|
|
logger.debug(
|
|
f"Lead ID: {instance.pk} not found in database when checking for status change. "
|
|
f"This might occur during initial object creation. Skipping status history tracking."
|
|
)
|
|
pass # Ignore if the lead doesn't exist (e.g., during initial creation)
|
|
|
|
|
|
# @receiver(post_save, sender=models.Lead)
|
|
# def notify_assigned_staff(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal handler that sends a notification to the staff member when a new lead is assigned.
|
|
# This function is triggered when a Lead instance is saved. If the lead has been assigned
|
|
# to a staff member, it creates a Notification object, notifying the staff member of the
|
|
# new assignment.
|
|
|
|
# :param sender: The model class that sent the signal.
|
|
# :param instance: The instance of the model that was saved.
|
|
# :param created: A boolean indicating whether a new instance was created.
|
|
# :param kwargs: Additional keyword arguments.
|
|
# :return: None
|
|
# """
|
|
# if instance.staff: # Check if the lead is assigned
|
|
# models.Notification.objects.create(
|
|
# user=instance.staff.staff_member.user,
|
|
# message=f"You have been assigned a new lead: {instance.full_name}.",
|
|
# )
|
|
|
|
|
|
@receiver(post_save, sender=models.CarReservation)
|
|
def update_car_status_on_reservation_create(sender, instance, created, **kwargs):
|
|
"""
|
|
Signal handler to update the status of a car upon the creation of a car reservation.
|
|
This function is triggered when a new instance of a CarReservation is created and saved
|
|
to the database. It modifies the status of the associated car to reflect the D status.
|
|
|
|
:param sender: The model class that sends the signal (CarReservation).
|
|
:param instance: The specific instance of the CarReservation that triggered the signal.
|
|
:param created: A boolean indicating whether the CarReservation instance was created.
|
|
:param kwargs: Additional keyword arguments passed by the signal.
|
|
:return: None
|
|
"""
|
|
if created:
|
|
car = instance.car
|
|
car.status = models.CarStatusChoices.RESERVED
|
|
car.save()
|
|
|
|
|
|
@receiver(post_delete, sender=models.CarReservation)
|
|
def update_car_status_on_reservation_delete(sender, instance, **kwargs):
|
|
"""
|
|
Signal handler that updates the status of a car to available when a car reservation
|
|
is deleted. If there are no active reservations associated with the car, the car's
|
|
status is updated to 'AVAILABLE'. This ensures the car's status accurately reflects
|
|
its reservability.
|
|
|
|
:param sender: The model class that triggers the signal (should always be
|
|
models.CarReservation in this context).
|
|
:type sender: type
|
|
:param instance: The instance of the deleted reservation.
|
|
:type instance: models.CarReservation
|
|
:param kwargs: Additional keyword arguments.
|
|
:type kwargs: dict
|
|
"""
|
|
car = instance.car
|
|
# Check if there are no active reservations for the car
|
|
if not car.reservations.filter(reserved_until__gt=now()).exists():
|
|
car.status = models.CarStatusChoices.AVAILABLE
|
|
car.save()
|
|
|
|
|
|
@receiver(post_save, sender=models.CarReservation)
|
|
def update_car_status_on_reservation_update(sender, instance, **kwargs):
|
|
"""
|
|
Handles the post-save signal for CarReservation model, updating the associated
|
|
car's status based on the reservation's activity status. If the reservation is
|
|
active, the car's status is updated to RESERVED. If the reservation is not
|
|
active, the car's status is set to AVAILABLE if there are no other active
|
|
reservations for the car.
|
|
|
|
:param sender: The model class that sent the signal.
|
|
:param instance: The CarReservation instance that triggered the signal.
|
|
:param kwargs: Additional keyword arguments passed by the signal.
|
|
:return: None
|
|
"""
|
|
car = instance.car
|
|
if instance.is_active:
|
|
car.status = models.CarStatusChoices.RESERVED
|
|
else:
|
|
if not car.reservations.filter(reserved_until__gt=now()).exists():
|
|
car.status = models.CarStatusChoices.AVAILABLE
|
|
car.save()
|
|
|
|
|
|
@receiver(post_save, sender=models.Dealer)
|
|
def create_dealer_settings(sender, instance, created, **kwargs):
|
|
"""
|
|
Triggered when a `Dealer` instance is saved. This function creates corresponding
|
|
`DealerSettings` for a newly created `Dealer` instance. The function assigns
|
|
default accounts for invoices and bills based on the role of the accounts
|
|
retrieved from the associated entity of the `Dealer`.
|
|
|
|
:param sender: The model class that triggered the signal, specifically `Dealer`.
|
|
:type sender: Type
|
|
:param instance: The actual instance of the `Dealer` that was saved.
|
|
:type instance: models.Dealer
|
|
:param created: A boolean indicating whether a new instance was created.
|
|
`True` if the instance was newly created; otherwise, `False`.
|
|
:type created: bool
|
|
:param kwargs: Additional keyword arguments passed by the signal.
|
|
:type kwargs: dict
|
|
:return: None
|
|
"""
|
|
if created:
|
|
models.VatRate.objects.create(dealer=instance)
|
|
models.DealerSettings.objects.create(
|
|
dealer=instance,
|
|
invoice_cash_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.ASSET_CA_CASH)
|
|
.first(),
|
|
invoice_prepaid_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.ASSET_CA_RECEIVABLES)
|
|
.first(),
|
|
invoice_unearned_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.LIABILITY_CL_DEFERRED_REVENUE)
|
|
.first(),
|
|
bill_cash_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.ASSET_CA_CASH)
|
|
.first(),
|
|
bill_prepaid_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.ASSET_CA_PREPAID)
|
|
.first(),
|
|
bill_unearned_account=instance.entity.get_all_accounts()
|
|
.filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
|
|
.first(),
|
|
)
|
|
|
|
|
|
# @receiver(post_save, sender=EstimateModel)
|
|
# def update_estimate_status(sender, instance,created, **kwargs):
|
|
|
|
# items = instance.get_itemtxs_data()[0].all()
|
|
# total = sum([Decimal(item.item_model.additional_info['car_finance']["selling_price"]) * Decimal(item.ce_quantity) for item in items])
|
|
|
|
# @receiver(post_save, sender=models.Schedule)
|
|
# def create_activity_on_schedule_creation(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# models.Activity.objects.create(
|
|
# content_object=instance,
|
|
# activity_type='Schedule Created',
|
|
# created_by=instance.scheduled_by,
|
|
# notes=f"New schedule created for {instance.purpose} with {instance.lead.full_name} on {instance.scheduled_at}."
|
|
# )
|
|
|
|
# @receiver(post_save, sender=models.Staff)
|
|
# def check_users_quota(sender, instance, **kwargs):
|
|
# quota_dict = get_user_quota(instance.dealer.user)
|
|
# allowed_users = quota_dict.get("Users")
|
|
# if allowed_users is None:
|
|
# raise ValidationError(_("The user quota for staff members is not defined. Please contact support."))
|
|
# current_staff_count = instance.dealer.staff.count()
|
|
# if current_staff_count > allowed_users:
|
|
# raise ValidationError(_("You have reached the maximum number of staff users allowed for your plan."))
|
|
# @receiver(post_save, sender=models.Dealer)
|
|
# def create_vat(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal receiver that listens to the `post_save` signal for the `Dealer` model
|
|
# and handles the creation of a `VatRate` instance if it does not already exist.
|
|
|
|
# This function ensures that a default VAT rate is created with a specified rate
|
|
# and is marked as active. It is connected to the Django signals framework and
|
|
# automatically executes whenever a `Dealer` instance is saved.
|
|
|
|
# :param sender: The model class that triggered the signal (in this case, `Dealer`).
|
|
# :param instance: The instance of the model being saved.
|
|
# :param created: Boolean indicating whether a new instance was created.
|
|
# :param kwargs: Additional keyword arguments passed by the signal.
|
|
# :return: None
|
|
# """
|
|
# VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
|
|
|
|
|
|
# @receiver(post_save, sender=models.Dealer)
|
|
# def create_make_ledger_accounts(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal receiver that creates ledger accounts for car makes associated with a dealer when a new dealer instance
|
|
# is created. This function listens to the `post_save` signal of the `Dealer` model and automatically generates
|
|
# new ledger accounts for all car makes, associating them with the given dealer's entity.
|
|
|
|
# :param sender: The model class (`Dealer`) that triggered the signal.
|
|
# :type sender: Type[models.Dealer]
|
|
# :param instance: The instance of the `Dealer` model that triggered the signal.
|
|
# :param created: A boolean indicating whether a new `Dealer` instance was created.
|
|
# :type created: bool
|
|
# :param kwargs: Additional keyword arguments passed by the signal.
|
|
# :return: None
|
|
# """
|
|
# if created:
|
|
# entity = instance.entity
|
|
# coa = entity.get_default_coa()
|
|
|
|
# for make in models. ke.objects.all():
|
|
# last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).order_by('-created').first()
|
|
# if len(last_account.code) == 4:
|
|
# code = f"{int(last_account.code)}{1:03d}"
|
|
# elif len(last_account.code) > 4:
|
|
# code = f"{int(last_account.code)+1}"
|
|
# entity.create_account(
|
|
# name=make.name,
|
|
# code=code,
|
|
# role=roles.ASSET_CA_RECEIVABLES,
|
|
# coa_model=coa,
|
|
# balance_type="credit",b vgbvf vh cbgl;;;
|
|
# active=True
|
|
# )
|
|
|
|
|
|
# @receiver(post_save, sender=VendorModel)
|
|
# def create_vendor_accounts(sender, instance, created, **kwargs):Dealer)
|
|
# if created:
|
|
# entity = instance.entity_model
|
|
# coa = entity.get_default_coa()
|
|
|
|
# last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first()
|
|
# if len(last_account.code) == 4:
|
|
# code = f"{int(last_account.path)}{1:03d}"
|
|
# elif len(last_account.code) > 4:
|
|
# code = f"{int(last_account.path)+1}"
|
|
# entity.create_account(
|
|
# name=instance.vendor_name,
|
|
# code=code,
|
|
# role=roles.LIABILITY_CL_ACC_PAYABLE,
|
|
# coa_model=coa,
|
|
# balance_type="credit",
|
|
# active=True
|
|
# )
|
|
|
|
|
|
# def save_journal(car_finance, ledger, vendor):
|
|
# """
|
|
# Saves a journal entry pertaining to a car finance transaction for a specific ledger and vendor.
|
|
|
|
# This function ensures that relevant accounts are updated to record financial transactions. It handles
|
|
# debiting of the inventory account and crediting of the vendor account to maintain accurate bookkeeping.
|
|
# Additionally, it creates vendor accounts dynamically if required and ties the created journal entry to
|
|
# the ledger passed as a parameter. All transactions adhere to the ledger's entity-specific Chart of
|
|
# Accounts (COA) configuration.
|
|
|
|
# :param car_finance: Instance of the car finance object containing details about the financed car
|
|
# and its associated costs.
|
|
# :type car_finance: `CarFinance` object
|
|
# :param ledger: Ledger instance to which the journal entry is tied. This ledger must provide
|
|
# entity-specific details, including its COA and related accounts.
|
|
# :type ledger: `Ledger` object
|
|
# :param vendor: Vendor instance representing the supplier or vendor related to the car finance
|
|
# transaction. This vendor is used to derive or create the vendor account in COA.
|
|
# :type vendor: `Vendor` object
|
|
|
|
# :return: None
|
|
# """
|
|
# entity = ledger.entity
|
|
# coa = entity.get_default_coa()
|
|
# journal = JournalEntryModel.objects.create(
|
|
# posted=False,
|
|
# description=f"Finances of Car:{car_finance.car.vin} for Vendor:{car_finance.car.vendor.name}",
|
|
# ledger=ledger,
|
|
# locked=False,
|
|
# origin="Payment",
|
|
# )
|
|
# ledger.additional_info["je_number"] = journal.je_number
|
|
# ledger.save()
|
|
|
|
# inventory_account = (
|
|
# entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first()
|
|
# )
|
|
# vendor_account = entity.get_default_coa_accounts().filter(name=vendor.name).first()
|
|
|
|
# if not vendor_account:
|
|
# last_account = (
|
|
# entity.get_all_accounts()
|
|
# .filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
|
|
# .order_by("-created")
|
|
# .first()
|
|
# )
|
|
# if len(last_account.code) == 4:
|
|
# code = f"{int(last_account.code)}{1:03d}"
|
|
# elif len(last_account.code) > 4:
|
|
# code = f"{int(last_account.code) + 1}"
|
|
|
|
# vendor_account = entity.create_account(
|
|
# name=vendor.name,
|
|
# code=code,
|
|
# role=roles.LIABILITY_CL_ACC_PAYABLE,
|
|
# coa_model=coa,
|
|
# balance_type="credit",
|
|
# active=True,
|
|
# )
|
|
# additional_services_account = (
|
|
# entity.get_default_coa_accounts()
|
|
# .filter(name="Additional Services", role=roles.COGS)
|
|
# .first()
|
|
# )
|
|
|
|
# # Debit Inventory Account
|
|
# TransactionModel.objects.create(
|
|
# journal_entry=journal,
|
|
# account=inventory_account,
|
|
# amount=car_finance.cost_price,
|
|
# tx_type="debit",
|
|
# )
|
|
|
|
# # Credit Vendor Account
|
|
# TransactionModel.objects.create(
|
|
# journal_entry=journal,
|
|
# account=vendor_account,
|
|
# amount=car_finance.cost_price,
|
|
# tx_type="credit",
|
|
# )
|
|
|
|
|
|
# @receiver(post_save, sender=models.CarFinance)
|
|
# def update_finance_cost(sender, instance, created, **kwargs):
|
|
# """
|
|
# Signal to handle `post_save` functionality for the `CarFinance` model. This function
|
|
# creates or updates financial records related to a car's finance details in the ledger
|
|
# associated with the car's vendor and dealer. For newly created instances, a ledger is
|
|
# created or retrieved, and the `save_journal` function is executed to log the financial
|
|
# transactions.
|
|
|
|
# This function also has commented-out logic to handle updates for already created
|
|
# instances, including journal updates or adjustments to existing financial transactions.
|
|
|
|
# :param sender: Model class that triggered the signal
|
|
# :type sender: Model
|
|
# :param instance: Instance of the `CarFinance` model passed to the signal
|
|
# :type instance: CarFinance
|
|
# :param created: Boolean value indicating if the instance was created (`True`) or updated (`False`)
|
|
# :type created: bool
|
|
# :param kwargs: Arbitrary keyword arguments passed to the signal
|
|
# :type kwargs: dict
|
|
# :return: None
|
|
# """
|
|
# if created:
|
|
# entity = instance.car.dealer.entity
|
|
# vendor = instance.car.vendor
|
|
# vin = instance.car.vin if instance.car.vin else ""
|
|
# make = instance.car.id_car_make.name if instance.car.id_car_make else ""
|
|
# model = instance.car.id_car_model.name if instance.car.id_car_model else ""
|
|
# year = instance.car.year
|
|
# vendor_name = vendor.name if vendor else ""
|
|
|
|
# name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
|
|
# ledger, _ = LedgerModel.objects.get_or_create(name=name, entity=entity)
|
|
# save_journal(instance, ledger, vendor)
|
|
|
|
# # if not created:
|
|
# # if ledger.additional_info.get("je_number"):
|
|
# # journal = JournalEntryModel.objects.filter(je_number=ledger.additional_info.get("je_number")).first()
|
|
# # journal.description = f"Finances of Car:{instance.car.vin} for Vendor:{instance.car.vendor.vendor_name}"
|
|
# # journal.save()
|
|
# # debit = journal.get_transaction_queryset().filter(tx_type='debit').first()
|
|
# # credit = journal.get_transaction_queryset().filter(tx_type='credit').first()
|
|
# # if debit and credit:
|
|
# # if journal.is_locked():
|
|
# # journal.mark_as_unlocked()
|
|
# # journal.save()
|
|
# # debit.amount = instance.cost_price
|
|
# # credit.amount = instance.cost_price
|
|
# # debit.save()
|
|
# # credit.save()
|
|
# # else:
|
|
# # save_journal(instance,ledger,vendor,journal=journal)
|
|
# # else:
|
|
# # save_journal(instance,ledger,vendor)
|
|
# # else:
|
|
# # save_journal(instance,ledger,vendor)
|
|
|
|
|
|
@receiver(post_save, sender=BillModel)
|
|
def save_po(sender, instance, created, **kwargs):
|
|
try:
|
|
if instance.is_paid() and instance.itemtransactionmodel_set.first().po_model:
|
|
instance.itemtransactionmodel_set.first().po_model.save()
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
@receiver(post_save, sender=PurchaseOrderModel)
|
|
def create_po_item_upload(sender, instance, created, **kwargs):
|
|
if instance.po_status == "fulfilled" or instance.po_status == "approved":
|
|
for item in instance.get_itemtxs_data()[0]:
|
|
dealer = models.Dealer.objects.get(entity=instance.entity)
|
|
if item.bill_model and item.bill_model.is_paid():
|
|
models.PoItemsUploaded.objects.update_or_create(
|
|
dealer=dealer,
|
|
po=instance,
|
|
item=item,
|
|
defaults={"status": instance.po_status},
|
|
)
|
|
|
|
# po_item = models.PoItemsUploaded.objects.get_or_create(
|
|
# dealer=dealer, po=instance, item=item,
|
|
# defaults={
|
|
# "status":instance.po_status
|
|
# }
|
|
# )
|
|
|
|
|
|
# @receiver(post_save, sender=models.Staff)
|
|
# def add_service_to_staff(sender, instance, created, **kwargs):
|
|
# if created:
|
|
# for service in Service.objects.all():
|
|
# instance.services_offered.add(service)
|
|
|
|
|
|
##########################################################
|
|
######################Notification########################
|
|
##########################################################
|
|
|
|
|
|
# @receiver(post_save, sender=PurchaseOrderModel)
|
|
# def create_po_fulfilled_notification(sender, instance, created, **kwargs):
|
|
# if instance.po_status == "fulfilled":
|
|
# dealer = models.Dealer.objects.get(entity=instance.entity)
|
|
# accountants = (
|
|
# models.CustomGroup.objects.filter(dealer=dealer, name="Inventory")
|
|
# .first()
|
|
# .group.user_set.exclude(email=dealer.user.email)
|
|
# .distinct()
|
|
# )
|
|
# for accountant in accountants:
|
|
# models.Notification.objects.create(
|
|
# user=accountant,
|
|
# message=f"""
|
|
# New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
|
|
# <a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
|
# """,
|
|
# )
|
|
|
|
|
|
@receiver(post_save, sender=models.Car)
|
|
def car_created_notification(sender, instance, created, **kwargs):
|
|
if created:
|
|
accountants = (
|
|
models.CustomGroup.objects.filter(
|
|
dealer=instance.dealer, name__in=["Accountant"]
|
|
)
|
|
.first()
|
|
.group.user_set.all()
|
|
.distinct()
|
|
)
|
|
managers = (
|
|
models.CustomGroup.objects.filter(
|
|
dealer=instance.dealer, name__in=["Manager"]
|
|
)
|
|
.first()
|
|
.group.user_set.all()
|
|
.distinct()
|
|
)
|
|
|
|
recipients = accountants.union(managers)
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
New Car {car_make}-{car_model}-{year}-{vin} has been added to the inventory.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(
|
|
car_make=instance.id_car_make,
|
|
car_model=instance.id_car_model,
|
|
year=instance.year,
|
|
vin=instance.vin,
|
|
url=instance.get_absolute_url(),
|
|
),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=PurchaseOrderModel)
|
|
def po_fullfilled_notification(sender, instance, created, **kwargs):
|
|
if instance.is_fulfilled():
|
|
dealer = models.Dealer.objects.get(entity=instance.entity)
|
|
recipients = User.objects.filter(
|
|
groups__customgroup__dealer=dealer,
|
|
groups__customgroup__name__in=["Manager", "Inventory"],
|
|
).distinct()
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
PO {po_number} has been fulfilled.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(
|
|
po_number=instance.po_number,
|
|
url=reverse(
|
|
"purchase_order_detail",
|
|
kwargs={
|
|
"dealer_slug": dealer.slug,
|
|
"entity_slug": instance.entity.slug,
|
|
"pk": instance.pk,
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=models.Vendor)
|
|
def vendor_created_notification(sender, instance, created, **kwargs):
|
|
if created:
|
|
recipients = User.objects.filter(
|
|
groups__customgroup__dealer=instance.dealer,
|
|
groups__customgroup__name__in=["Manager", "Inventory"],
|
|
).distinct()
|
|
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
New Vendor {vendor_name} has been added to dealer {dealer_name}.
|
|
"""
|
|
).format(vendor_name=instance.name, dealer_name=instance.dealer.name),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=models.SaleOrder)
|
|
def sale_order_created_notification(sender, instance, created, **kwargs):
|
|
if created:
|
|
recipients = (
|
|
models.CustomGroup.objects.filter(dealer=instance.dealer, name="Accountant")
|
|
.first()
|
|
.group.user_set.exclude(email=instance.dealer.user.email)
|
|
.distinct()
|
|
)
|
|
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
New Sale Order has been added for estimate:{estimate_number}.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(
|
|
estimate_number=instance.estimate.estimate_number,
|
|
url=reverse(
|
|
"estimate_detail",
|
|
kwargs={
|
|
"dealer_slug": instance.dealer.slug,
|
|
"pk": instance.estimate.pk,
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=models.Lead)
|
|
def lead_created_notification(sender, instance, created, **kwargs):
|
|
if created:
|
|
if instance.staff:
|
|
models.Notification.objects.create(
|
|
user=instance.staff.user,
|
|
message=_(
|
|
"""
|
|
New Lead has been added.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(url=instance.get_absolute_url()),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=EstimateModel)
|
|
def estimate_in_review_notification(sender, instance, created, **kwargs):
|
|
if instance.is_review():
|
|
dealer = models.Dealer.objects.get(entity=instance.entity)
|
|
recipients = (
|
|
models.CustomGroup.objects.filter(dealer=dealer, name="Manager")
|
|
.first()
|
|
.group.user_set.exclude(email=dealer.user.email)
|
|
.distinct()
|
|
)
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
Estimate {estimate_number} is in review.
|
|
Please review and approve it at your earliest convenience.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(
|
|
estimate_number=instance.estimate_number,
|
|
url=reverse(
|
|
"estimate_detail",
|
|
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=EstimateModel)
|
|
def estimate_in_approve_notification(sender, instance, created, **kwargs):
|
|
if instance.is_approved():
|
|
dealer = models.Dealer.objects.get(entity=instance.entity)
|
|
|
|
recipient = models.ExtraInfo.objects.filter(
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
|
object_id=instance.pk,
|
|
).first()
|
|
if not recipient:
|
|
return
|
|
models.Notification.objects.create(
|
|
user=recipient.related_object.user,
|
|
message=_(
|
|
"""
|
|
Estimate {estimate_number} has been approved.
|
|
<a href="{url}" target="_blank">View</a>
|
|
"""
|
|
).format(
|
|
estimate_number=instance.estimate_number,
|
|
url=reverse(
|
|
"estimate_detail",
|
|
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@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)
|
|
.distinct()
|
|
)
|
|
|
|
for recipient in recipients:
|
|
models.Notification.objects.create(
|
|
user=recipient,
|
|
message=_(
|
|
"""
|
|
Bill {bill_number} is in review,please review and approve it
|
|
<a href="{url}" target="_blank">View</a>.
|
|
"""
|
|
).format(
|
|
bill_number=instance.bill_number,
|
|
url=reverse(
|
|
"bill-update",
|
|
kwargs={
|
|
"dealer_slug": dealer.slug,
|
|
"entity_slug": dealer.entity.slug,
|
|
"bill_pk": instance.pk,
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
# @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)
|
|
# .distinct()
|
|
# )
|
|
|
|
# for recipient in recipients:
|
|
# models.Notification.objects.create(
|
|
# user=recipient,
|
|
# message=_(
|
|
# """
|
|
# Bill {bill_number} has been approved.
|
|
# <a href="{url}" target="_blank">View</a>.
|
|
# please complete the bill payment.
|
|
# """
|
|
# ).format(
|
|
# bill_number=instance.bill_number,
|
|
# url=reverse(
|
|
# "bill-detail",
|
|
# kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
|
# ),
|
|
# ),
|
|
# )
|
|
|
|
|
|
@receiver(post_save, sender=models.Ticket)
|
|
def send_ticket_notification(sender, instance, created, **kwargs):
|
|
if created:
|
|
subject = f"New Support Ticket: {instance.subject}"
|
|
message = f"""
|
|
A new support ticket has been created:
|
|
|
|
Ticket ID: #{instance.id}
|
|
Subject: {instance.subject}
|
|
Priority: {instance.get_priority_display()}
|
|
Description:
|
|
{instance.description}
|
|
|
|
Please log in to the admin panel to respond.
|
|
"""
|
|
|
|
send_email(
|
|
settings.DEFAULT_FROM_EMAIL,
|
|
[settings.SUPPORT_EMAIL],
|
|
subject,
|
|
message,
|
|
)
|
|
else:
|
|
models.Notification.objects.create(
|
|
user=instance.dealer.user,
|
|
message=_(
|
|
"""
|
|
Support Ticket #{ticket_number} has been updated.
|
|
<a href="{url}" target="_blank">View</a>.
|
|
"""
|
|
).format(
|
|
ticket_number=instance.pk,
|
|
url=reverse(
|
|
"ticket_detail",
|
|
kwargs={
|
|
"dealer_slug": instance.dealer.slug,
|
|
"ticket_id": instance.pk,
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=models.CarColors)
|
|
def handle_car_image(sender, instance, created, **kwargs):
|
|
"""
|
|
Simple handler for car image generation
|
|
"""
|
|
try:
|
|
# Create or get car image record
|
|
car = instance.car
|
|
car.hash = car.get_hash
|
|
car.save()
|
|
# car_image, created = models.CarImage.objects.get_or_create(
|
|
# car=car, defaults={"image_hash": car.get_hash}
|
|
# )
|
|
|
|
# Check for existing image with same hash
|
|
existing = os.path.exists(
|
|
os.path.join(settings.MEDIA_ROOT, "car_images", car.get_hash + ".png")
|
|
)
|
|
# existing = (
|
|
# models.CarImage.objects.filter(
|
|
# image_hash=car.get_hash, image__isnull=False
|
|
# )
|
|
# .first()
|
|
# )
|
|
|
|
if existing:
|
|
logger.info(f"Found existing image for car {car.vin}")
|
|
# Copy existing image
|
|
# car_image.image.save(existing.image.name, existing.image.file, save=True)
|
|
logger.info(f"Reused image for car {car.vin}")
|
|
else:
|
|
logger.info(f"Generating image for car {car.vin}")
|
|
# Schedule async generation
|
|
async_task(
|
|
"inventory.tasks.generate_car_image_task",
|
|
car.pk,
|
|
task_name=f"generate_car_image_{car.vin}",
|
|
)
|
|
# async_task(
|
|
# "inventory.tasks.generate_car_image_task",
|
|
# car_image.id,
|
|
# task_name=f"generate_car_image_{car.vin}",
|
|
# )
|
|
logger.info(f"Scheduled image generation for car {car.vin}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error handling car image for {car.vin}: {e}")
|
|
|
|
|
|
@receiver(post_save, sender=models.UserRegistration)
|
|
def handle_user_registration(sender, instance, created, **kwargs):
|
|
if created:
|
|
send_email(
|
|
settings.DEFAULT_FROM_EMAIL,
|
|
instance.email,
|
|
"Account Registration",
|
|
"""
|
|
Thank you for registering with us. We will contact you shortly to complete your application.
|
|
شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك.
|
|
""",
|
|
)
|
|
|
|
if instance.is_created:
|
|
logger.info(f"User account created: {instance.email}, sending email")
|
|
# instance.create_account()
|
|
send_email(
|
|
settings.DEFAULT_FROM_EMAIL,
|
|
instance.email,
|
|
"Account Created",
|
|
f"""
|
|
Dear {instance.name},
|
|
|
|
Your account has been created and you can login with your email {instance.email} and password: {instance.password}.
|
|
Please login to the website to complete your profile and start using our services.
|
|
|
|
Thank you for choosing us.
|
|
|
|
عزيزي {instance.name},
|
|
|
|
لقد تم إنشاء حسابك والآن يمكنك تسجيل الدخول باستخدام بريدك الإلكتروني {instance.email} وكلمة المرور: {instance.password}.
|
|
يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا.
|
|
|
|
شكرا لاختيارك لنا.
|
|
""",
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=ChartOfAccountModel)
|
|
def handle_chart_of_account(sender, instance, created, **kwargs):
|
|
if created:
|
|
entity = instance.entity
|
|
dealer = instance.entity.admin.dealer
|
|
# Create UOMs (minimal, no logging per item)
|
|
if not entity.get_uom_all():
|
|
for code, name in models.UnitOfMeasure.choices:
|
|
entity.create_uom(name=name, unit_abbr=code)
|
|
|
|
try:
|
|
# Schedule async account creation AFTER commit
|
|
transaction.on_commit(
|
|
lambda: async_task(
|
|
"inventory.tasks.create_coa_accounts",
|
|
dealer_id=dealer.pk,
|
|
coa_slug=instance.slug,
|
|
hook="inventory.hooks.check_create_coa_accounts",
|
|
ack_failure=True,
|
|
)
|
|
)
|
|
# async_task(
|
|
# func="inventory.tasks.create_coa_accounts",
|
|
# dealer_id=dealer.pk, # Pass ID instead of object
|
|
# coa_slug=instance.slug,
|
|
# hook="inventory.hooks.check_create_coa_accounts",
|
|
# ack_failure=True, # Ensure task failures are acknowledged
|
|
# sync=False # Explicitly set to async
|
|
# )
|
|
except Exception as e:
|
|
logger.error(f"Error handling chart of account: {e}")
|