Arquitectura

Modelo de base de datos

Los modelos fundamentales de Preciosa se definen en la aplicación precios </preciosa/precios/models.py>. Un diagrama simplificado es el siguiente:

http://yuml.me/d04f8709

Algoritmos de «precios»

Como se observa, el relevamiento de un precio está asociado a un producto y a una sucursal específica (y, opcionalmente, a un usuario), además de su fecha de relevamiento.

Al ser improbable la existencia de precios para cualquier producto en todas las sucursales, la consulta del precio de un producto para una sucursal en particular se realiza a través del método PrecioManager.mas_probables(), que realiza un degradado de información:

  1. Si existe un precio relevado para el producto en la sucursal, dentro del rango de máximo de dias, se devuelve ese dato

  2. En caso de que no exista, se buscan precios para el producto en sucursales de la misma cadena o un radio definido.

  3. Si no hay precios en Sucursales, se buscan el precio del producto en la «Sucursal Online» de la Cadena.

  4. Si no hay sucursal online asociada o no existe un precio vigente, no se devuelve un resultado.

    def mas_probables(self, producto, sucursal, dias=None, radio=10):
        """
        Cuando no hay datos especificos de un
        producto para una sucursal (:meth:`PrecioManager.historico`),
        debe ofrecerse un precio más probable. Se calcula

         - Precio más nuevo para el producto en otras sucursales
           de la misma cadena en la ciudad y/o un radio de distancia si es dado

         - En su defecto, precio online de la cadena
        """
        qs = self.historico(producto, sucursal, dias)
        if len(qs) > 0:
            return qs

        qs = super(PrecioManager, self).get_queryset()

        # precios para sucursales de la misma cadena de la ciudad
        cercanas_ciudad = sucursal.cercanas(misma_cadena=True).values_list('id',
                                                                           flat=True)
        if radio:
            cercanas_radio = sucursal.cercanas(radio=radio,
                                               misma_cadena=True).values_list('id',
                                                                              flat=True)
        else:
            cercanas_radio = []

        cercanas = set(list(cercanas_ciudad) + list(cercanas_radio))
        qs = qs.filter(producto=producto,
                       sucursal__id__in=cercanas).distinct('precio')
        if qs.exists():
            return self._registro_precio(qs)

        qs = super(PrecioManager, self).get_queryset()
        qs = qs.filter(producto=producto,
                       sucursal__cadena=sucursal.cadena,
                       sucursal__online=True).distinct('precio')
        # precios online
        if qs.exists():
            return self._registro_precio(qs)
        return []

Análogamente se calculan los mejores precios. Dado un producto, una ubicación (o sucursal) y radio de distancia, se obtiene una lista de los mejores precios en la zona para ese producto.

    def mejores(self, producto, ciudad=None, punto_o_sucursal=None,
                radio=None, dias=None, limite=5):
        """
        devuelve una lista de instancias Precio para el producto,
        ordenados por menor precio (importe) para
        un determinado producto y un radio de distancia o ciudad.

        Sólo considera el último precio en cada sucursal.
        """

        #si tiene puto.. tenemos que tener el radio
        if punto_o_sucursal and not radio:
            raise ValueError(
                'Si se especifica el punto o sucursal debe proveer el radio')

        if radio and not punto_o_sucursal and not ciudad:
            raise ValueError(
                'Si se especifica el radio debe proveer el punto o sucursal')

        #si no tenemos una ciudad o un punto con radio
        if not ciudad and not radio:
            raise ValueError(
                'Debe proveer una ciudad o un radio en kilometros')

        qs = super(PrecioManager,
                   self).get_queryset().filter(producto=producto, activo__isnull=False)

        if dias:
            desde = timezone.now() - timedelta(days=dias)
            qs = qs.filter(created__gte=desde)

        if ciudad:
            if isinstance(ciudad, City):
                ciudad = ciudad.id
            qs = qs.filter(sucursal__ciudad__id=ciudad).distinct(
                'sucursal')[:limite]
        elif radio:
            if isinstance(punto_o_sucursal, Sucursal):
                punto = punto_o_sucursal.ubicacion
            else:
                punto = punto_o_sucursal
            cercanas = Sucursal.objects.filter(ubicacion__distance_lte=(punto,
                                                                        D(km=radio)))
            cercanas = cercanas.values_list('id', flat=True)
            qs = qs.filter(sucursal__id__in=cercanas).distinct(
                'sucursal')[:limite]

        if qs.exists():
            return sorted(qs, key=lambda i: i.precio)
        return []