haikal/inventory/signals.py
2025-09-24 11:07:31 +03:00

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}")