Search code examples
djangoforeign-keysunique-constraintcomposite-primary-key

Type Error while populating a model in Django with UniqueConstraint


I'm trying to build up my first project in Django and I'm having some issues while creating an entry into my model Prevision (including sales forecasts), which has a uniqueConstraint to ensure that there is only one forecast between a Supplier (model "Representada") and a Customer (model "Cliente") for a give year (field "Ano").

My models.py file is as follows:

from django.db import models
from django.db.models import CheckConstraint, Q, F, UniqueConstraint
from django.db.models.signals import pre_save
from django.dispatch import receiver
from datetime import datetime

# Create your models here.
class Empresa (models.Model):
    CIF = models.CharField(max_length=9, unique=True, blank=True)
    Nombre = models.CharField(max_length=64)
    Telefono = models.CharField(max_length=16, blank=True)
    Email = models.EmailField(blank=True)
    Direccion = models.CharField(max_length=64)
    Ciudad = models.CharField(max_length=32)
    CP = models.PositiveIntegerField(default=1)
    Provincia = models.IntegerField(default=1)
    Contacto = models.CharField(max_length=64, blank=True)
    Observaciones = models.TextField(blank=True)
    
    def __str__(self):
        return f"{self.Nombre}"
    
    class Meta:
        abstract = True
        verbose_name_plural = "Empresas"
        constraints = [
        ]
    
class Representada (Empresa):
    Fecha_Alta = models.DateField()
    Fecha_Baja = models.DateField(blank=True, null=True)
    Porcentaje_Comision = models.DecimalField(max_digits=5, decimal_places=2)
    
    class Meta:
        verbose_name_plural = "Representadas"
        constraints = [
            CheckConstraint(
                check = Q(Fecha_Baja__isnull=True)|Q(Fecha_Alta__lte=F('Fecha_Baja')),
                name = 'Comprobar fecha de alta y baja de representada',
            ),
        ]
    
class Cliente (Empresa):
    Hay_Toro = models.BooleanField(blank=True)
    Inicio_Horario_Descarga = models.TimeField(blank=True, null=True)
    Fin_Horario_Descarga = models.TimeField(blank=True, null=True)
    
    def __str__(self):
        return f"{self.Nombre} ({self.Ciudad})"
    
    class Meta:
        verbose_name_plural = "Clientes"
        constraints = [
            CheckConstraint(
                check = Q(Inicio_Horario_Descarga__isnull=True, Fin_Horario_Descarga__isnull=True)|Q(Inicio_Horario_Descarga__isnull=False, Fin_Horario_Descarga__isnull=False, Inicio_Horario_Descarga__lte=F('Fin_Horario_Descarga')),
                name = 'Comprobar horario de descarga',
            ),
        ]
    
class Prevision (models.Model):
    Ano = models.PositiveSmallIntegerField(default=datetime.now().year)
    Cliente = models.ForeignKey("Cliente", on_delete=models.CASCADE)
    Representada = models.ForeignKey("Representada", on_delete=models.CASCADE)
    Importe = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
    
    def __str__(self):
        return f"Año: {self.Ano} Representada: {self.Representada} Cliente: {self.Cliente}"
    
    class Meta:
        verbose_name_plural = "Previsiones"
        constraints = [
            UniqueConstraint(
                fields=['Ano', 'Cliente', 'Representada'], name='Primary_Key_Prevision'
            )
        ]
    
@receiver(pre_save, sender=Prevision)
def comprobar_ano_prevision(sender, instance, ** kwargs) :
    representada = Representada.objects.get(id = instance.Representada)
    if representada.Fecha_Baja is not NONE and representada.Fecha_Baja.year < instance.Año:
        raise Exception("No se puede generar una previsión para una representada dada de baja")
        

Once models are properly migrated, I enter into admin mode and register a Supplier (i.e. 'Representada') and a Customer (i.e. 'Cliente') with no issues. Once I have one of each registered in the database, I try to do the same with a forecast (i.e. 'Prevision'), however, when trying to do this via admin mode in the web browser I got a Server Error (500) in return.

Trying to understand a bit better what's going on, I run a shell to have a closer look to the error:

(Desarrollo) (...)\Desarrollo\Company>py manage.py shell
Python 3.12.4 (tags/v3.12.4:8e8a4ba, Jun  6 2024, 19:30:16) [MSC v.1940 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from BaseDatos.models import Representada, Prevision, Cliente
>>> Cliente.objects.all().values()
<QuerySet [{'id': 4, 'CIF': '2', 'Nombre': 'Cli1', 'Telefono': '', 'Email': '', 'Direccion': '2', 'Ciudad': '2', 'CP': 13300, 'Provincia': 13, 'Contacto': '', 'Observaciones': '', 'Hay_Toro': False, 'Inicio_Horario_Descarga': None, 'Fin_Horario_Descarga': None}]>
>>> Representada.objects.all().values()
<QuerySet [{'id': 3, 'CIF': '1', 'Nombre': 'Rep1', 'Telefono': '', 'Email': '', 'Direccion': '1', 'Ciudad': '1', 'CP': 13300, 'Provincia': 13, 'Contacto': '', 'Observaciones': '', 'Fecha_Alta': datetime.date(2024, 6, 26), 'Fecha_Baja': None, 'Porcentaje_Comision': Decimal('1.00')}]>
>>> Prevision.objects.all().values()
<QuerySet []>
>>> prev = Prevision(Ano='2023', Cliente=Cliente.objects.get(id='4'), Representada=Representada.objects.get(id='3'), Importe='100.00')
>>> prev.save()
Traceback (most recent call last):
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\fields\__init__.py", line 2117, in get_prep_value
    return int(value)
           ^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'Representada'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\base.py", line 822, in save
    self.save_base(
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\base.py", line 889, in save_base
    pre_save.send(
  File "(...)\Desarrollo\Lib\site-packages\django\dispatch\dispatcher.py", line 189, in send
    response = receiver(signal=self, sender=sender, **named)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Company\BaseDatos\models.py", line 290, in comprobar_ano_prevision
    representada = Representada.objects.get(id = instance.Representada)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\query.py", line 635, in get
    clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
                                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\query.py", line 1476, in filter
    return self._filter_or_exclude(False, args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\query.py", line 1494, in _filter_or_exclude
    clone._filter_or_exclude_inplace(negate, args, kwargs)
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\query.py", line 1501, in _filter_or_exclude_inplace
    self._query.add_q(Q(*args, **kwargs))
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\sql\query.py", line 1613, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\sql\query.py", line 1645, in _add_q
    child_clause, needed_inner = self.build_filter(
                                 ^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\sql\query.py", line 1559, in build_filter
    condition = self.build_lookup(lookups, col, value)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\sql\query.py", line 1389, in build_lookup
    lookup = lookup_class(lhs, rhs)
             ^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\lookups.py", line 30, in __init__
    self.rhs = self.get_prep_lookup()
               ^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\lookups.py", line 364, in get_prep_lookup
    return super().get_prep_lookup()
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\lookups.py", line 88, in get_prep_lookup
    return self.lhs.output_field.get_prep_value(self.rhs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "(...)\Desarrollo\Lib\site-packages\django\db\models\fields\__init__.py", line 2119, in get_prep_value
    raise e.__class__(
TypeError: Field 'id' expected a number but got <Representada: Rep1>.
>>>

If I try to use a number instead of an instance, as suggested by the TypeError, things don't go any better:

>>> prev = Prevision(Ano='2023', Cliente=Cliente.objects.get(id='4'), Representada='3', Importe='100.00')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "D:\Documents\Trabajo\00_Ofrevit\Desarrollo\Lib\site-packages\django\db\models\base.py", line 543, in __init__
    _setattr(self, field.name, rel_obj)
  File "D:\Documents\Trabajo\00_Ofrevit\Desarrollo\Lib\site-packages\django\db\models\fields\related_descriptors.py", line 284, in __set__
    raise ValueError(
ValueError: Cannot assign "'3'": "Prevision.Representada" must be a "Representada" instance.

I guess the issue must be pretty obvious for someone with a bit of experience and it should be some how related to the fact that Prevision has a uniqueConstraint to keep a pseudo-multiple field PK apart from the id key automatically inserted by Django, but I just cannot see where or why... Any advice will be much appreciated!


Solution

  • Look at your pre_save signal function:

    @receiver(pre_save, sender=Prevision)
    def comprobar_ano_prevision(sender, instance, **kwargs):
        representada = Representada.objects.get(id=instance.Representada)
        ...
    

    First trying to assign an instance as ID and later trying to access an instance field from an int (the error messages), so:

    Representada.objects.get(id=instance.Representada.id)