san4ezy / django_softdelete

MIT License
79 stars 21 forks source link

When I try to delete an object that is a ForeignKey of another, I get this error #28

Open juandaherrera opened 8 months ago

juandaherrera commented 8 months ago

This is my BaseModel:

from django.db import models
from django_softdelete.models import SoftDeleteModel
from django_userforeignkey.models.fields import UserForeignKey

class BaseModel(SoftDeleteModel):
    """
    Project main model class

    Args:
        models: django model class
    """

    created_at = models.DateTimeField(
        verbose_name="Fecha de creación", auto_now_add=True, editable=False
    )
    updated_at = models.DateTimeField(
        verbose_name="Fecha de actualización", auto_now=True, editable=False
    )

    created_by = UserForeignKey(
        verbose_name="Creado por",
        auto_user_add=True,
        related_name="+",
        editable=False,
    )
    updated_by = UserForeignKey(
        verbose_name="Actualizado por",
        auto_user=True,
        related_name="+",
        editable=False,
    )

    # all_objects = models.Manager()

    class Meta:
        abstract = True

This is the model I want to delete:

class Account(BaseModel):
    """
    This model is the representation of a user account. For example: A savings account at bank x, a credit card at bank y, etc.
    """

    type = models.ForeignKey(AccountType, on_delete=models.CASCADE, verbose_name='Tipo de Cuenta')
    currency = models.ForeignKey(
        Currency, on_delete=models.CASCADE, verbose_name='Moneda'
    )  # , default=1 <- COP debería ser el default
    name = models.CharField(max_length=80, verbose_name='Nombre')
    balance = models.DecimalField(
        max_digits=12,
        verbose_name='Saldo',
        decimal_places=4,
        null=True,
        blank=True,
        default=0,
    )
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, verbose_name='Usuario')
    is_shared = models.BooleanField(verbose_name='Cuenta compartida', default=False)
    family_group = models.ForeignKey(
        FamilyGroup,
        on_delete=models.CASCADE,
        verbose_name='Grupo Familiar',
        null=True,
        blank=True,
    )

    @property
    def balance_formatted(self):
        return utils.float_formatter(self.balance)

    @property
    def last_transaction(self):
        return self.transaction_set.order_by('-date').first()

    class Meta:
        verbose_name = 'Cuenta'
        verbose_name_plural = "Cuentas"
        ordering = ['family_group', 'user', 'type', 'name']

        constraints = [
            models.UniqueConstraint(
                fields=['user', 'name', 'family_group'],
                name='user_name_family_group_unique_account',
                condition=models.Q(deleted_at__isnull=True),
            )
        ]

    def __str__(self):
        return f'{self.name} ({self.balance:,.2f} {self.currency})'

This is the model that has an Account as ForeignKey:

class Transaction(BaseModel):
    category = models.ForeignKey(
        UserTrasactionCategory, on_delete=models.CASCADE, verbose_name='Categoría'
    )
    account = models.ForeignKey(Account, on_delete=models.CASCADE, verbose_name='Cuenta')
    amount = models.DecimalField(
        max_digits=12,
        verbose_name='Valor',
        decimal_places=4,
        null=True,
        blank=True,
        default=0,
    )
    date = models.DateTimeField(verbose_name='Fecha de Transacción', default=timezone.now)
    description = models.TextField(verbose_name='Descripción', null=True, blank=True)

    class Meta:
        verbose_name = 'Transacción'
        verbose_name_plural = "Transacciones"
        ordering = ['category', '-date', 'account']

    def __str__(self) -> str:
        return f'({self.category}) | {self.date} | {self.amount}'

When I have a transaction associated with an account and try to delete that account from the admin, I get the following error:

Environment:

Request Method: POST
Request URL: http://127.0.0.1:8000/admin/finance/account/3/delete/

Django Version: 5.0.2
Python Version: 3.11.5
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'django_userforeignkey',
 'widget_tweaks',
 'apps.users',
 'apps.core',
 'apps.finance',
 'django_extensions']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']

Traceback (most recent call last):
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 715, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/utils/decorators.py", line 188, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/utils/decorators.py", line 186, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/views/decorators/cache.py", line 80, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/contrib/admin/sites.py", line 240, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/utils/decorators.py", line 48, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/utils/decorators.py", line 188, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/utils/decorators.py", line 186, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 2164, in delete_view
    return self._delete_view(request, object_id, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 2200, in _delete_view
    self.delete_model(request, obj)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1264, in delete_model
    obj.delete()
    ^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django_softdelete/models.py", line 121, in delete
    self.__delete_related_one_to_many(field, strict, *args, **kwargs)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django_softdelete/models.py", line 351, in __delete_related_one_to_many
    self.__delete_related_object(
    ^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django_softdelete/models.py", line 283, in __delete_related_object
    related_object.save()
    ^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 822, in save
    self.save_base(
    ^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/db/models/base.py", line 889, in save_base
    pre_save.send(
    ^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/dispatch/dispatcher.py", line 189, in send
    response = receiver(signal=self, sender=sender, **named)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/apps/finance/signals.py", line 52, in update_account_balance
    old_amount = Transaction.objects.get(pk=instance.pk).amount
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/juanda/documents/projects/faminance/.venv/lib/python3.11/site-packages/django/db/models/query.py", line 649, in get
    raise self.model.DoesNotExist(
    ^

Exception Type: DoesNotExist at /admin/finance/account/3/delete/
Exception Value: Transaction matching query does not exist.

What's strange is that, when I first delete the transaction and then delete the account, there's no problem.

I'm using the 1.0.12 version of django-soft-delete

heysamtexas commented 2 months ago

I'm late to the party on this one, but maybe still relevant...

It looks like django-soft-delete package is trying to soft delete related objects, but the signal you’ve set up (update_account_balance) is causing an error when the related object (Transaction) is being accessed after it’s already been marked for deletion.

Possible Solutions:

  1. Modify the Signal Handling: Adjust your signal to account for soft-deleted objects. Ensure that when the object is soft-deleted, the signal doesn’t attempt to access the already deleted related object.
  2. Override the delete() Method: In your Account model, override the delete() method to handle related objects appropriately before triggering the delete operation, ensuring that the signal doesn’t fire inappropriately.
  3. Signal Condition: Add a condition to your signal to check if the object still exists (is not deleted) before performing operations on it.

These steps should help avoid the DoesNotExist exception you’re seeing when deleting the account with related transactions.

It might also be helpful to share the source of the signals.py file as well.