Source code for shuup.core.models._suppliers

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
import logging
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from filer.fields.image import FilerImageField
from jsonfield import JSONField
from parler.managers import TranslatableQuerySet
from parler.models import TranslatedFields
from typing import TYPE_CHECKING, Union

from shuup.core.fields import InternalIdentifierField
from shuup.core.modules import ModuleInterface
from shuup.utils.analog import define_log_model

from ._base import TranslatableShuupModel

if TYPE_CHECKING:
    from shuup.core.models import Shop


LOGGER = logging.getLogger(__name__)


class SupplierType(Enum):
    INTERNAL = 1
    EXTERNAL = 2

    class Labels:
        INTERNAL = _("internal")
        EXTERNAL = _("external")


class SupplierQueryset(TranslatableQuerySet):
    def not_deleted(self):
        return self.filter(deleted=False)

    def enabled(self, shop: Union["Shop", int] = None):
        """
        Filter the queryset to contain only enabled and approved suppliers.

        If `shop` is given, only approved suppliers
        for the given shop will be filtered.

        `shop` can be either a Shop instance or the shop's PK
        """
        queryset = self.filter(enabled=True, supplier_shops__is_approved=True).not_deleted()

        if shop:
            from shuup.core.models import Shop

            shop_id = shop.pk if isinstance(shop, Shop) else shop
            queryset = queryset.filter(supplier_shops__shop_id=shop_id)

        return queryset.distinct()


@python_2_unicode_compatible
class Supplier(ModuleInterface, TranslatableShuupModel):
    module_provides_key = "supplier_module"

    created_on = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name=_("created on"))
    modified_on = models.DateTimeField(auto_now=True, editable=False, db_index=True, verbose_name=_("modified on"))
    identifier = InternalIdentifierField(unique=True)
    name = models.CharField(
        verbose_name=_("name"),
        max_length=128,
        db_index=True,
        help_text=_(
            "The product supplier's name. " "You can enable suppliers to manage the inventory of stocked products."
        ),
    )
    type = EnumIntegerField(
        SupplierType,
        verbose_name=_("supplier type"),
        default=SupplierType.INTERNAL,
        help_text=_(
            "The supplier type indicates whether the products are supplied through an internal supplier or "
            "an external supplier, and which group this supplier belongs to."
        ),
    )
    stock_managed = models.BooleanField(
        verbose_name=_("stock managed"),
        default=False,
        help_text=_(
            "Enable this if this supplier will manage the inventory of the stocked products. Having a managed stock "
            "enabled is unnecessary if e.g. selling digital products that will never run out no matter how many are "
            "being sold. There are some other cases when it could be an unnecessary complication. This setting"
            "merely assigns a sensible default behavior, which can be overwritten on a product-by-product basis."
        ),
    )
    supplier_modules = models.ManyToManyField(
        "SupplierModule",
        blank=True,
        related_name="suppliers",
        verbose_name=_("supplier modules"),
        help_text=_(
            "Select the supplier module to use for this supplier. "
            "Supplier modules define the rules by which inventory is managed."
        ),
    )
    module_data = JSONField(blank=True, null=True, verbose_name=_("module data"))

    shops = models.ManyToManyField(
        "Shop",
        blank=True,
        related_name="suppliers",
        verbose_name=_("shops"),
        help_text=_("You can select which particular shops fronts the supplier should be available in."),
        through="SupplierShop",
    )
    enabled = models.BooleanField(
        default=True,
        verbose_name=_("enabled"),
        help_text=_(
            "Indicates whether this supplier is currently enabled. In order to participate fully, "
            "the supplier also needs to be `Approved`."
        ),
    )
    logo = FilerImageField(
        verbose_name=_("logo"), blank=True, null=True, on_delete=models.SET_NULL, related_name="supplier_logos"
    )
    contact_address = models.ForeignKey(
        "MutableAddress",
        related_name="supplier_addresses",
        verbose_name=_("contact address"),
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
    )
    options = JSONField(blank=True, null=True, verbose_name=_("options"))
    translations = TranslatedFields(description=models.TextField(blank=True, verbose_name=_("description")))
    slug = models.SlugField(
        verbose_name=_("slug"),
        max_length=255,
        blank=True,
        null=True,
        help_text=_(
            "Enter a URL slug for your supplier. Slug is user- and search engine-friendly short text "
            "used in a URL to identify and describe a resource. In this case it will determine "
            "what your supplier page URL in the browser address bar will look like. "
            "A default will be created using the supplier name."
        ),
    )
    deleted = models.BooleanField(default=False, verbose_name=_("deleted"))

    search_fields = ["name"]
    objects = SupplierQueryset.as_manager()

    def __str__(self):
        return self.name

[docs] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) return super(Supplier, self).save(*args, **kwargs)
[docs] def get_orderability_errors(self, shop_product, quantity, customer, *args, **kwargs): """ :param shop_product: Shop Product. :type shop_product: shuup.core.models.ShopProduct :param quantity: Quantity to order. :type quantity: decimal.Decimal :param contect: Ordering contact. :type contect: shuup.core.models.Contact :rtype: iterable[ValidationError] """ for module in self.modules: yield from module.get_orderability_errors( shop_product=shop_product, quantity=quantity, customer=customer, *args, **kwargs )
[docs] def get_stock_statuses(self, product_ids, *args, **kwargs): """ Return a dict of product stock statuses :param product_ids: Iterable of product IDs. :return: Dict of {product_id: ProductStockStatus} :rtype: dict[int, shuup.core.stocks.ProductStockStatus] """ return_dict = {} for module in self.modules: return_dict.update(module.get_stock_statuses(product_ids, *args, **kwargs)) return return_dict
[docs] def get_stock_status(self, product_id, *args, **kwargs): for module in self.modules: stock_status = module.get_stock_status(product_id, *args, **kwargs) if stock_status.handled: return stock_status
[docs] def get_suppliable_products(self, shop, customer): """ :param shop: Shop to check for suppliability. :type shop: shuup.core.models.Shop :param customer: Customer contact to check for suppliability. :type customer: shuup.core.models.Contact :rtype: list[int] """ return [ shop_product.pk for shop_product in self.shop_products.filter(shop=shop) if shop_product.is_orderable(self, customer, shop_product.minimum_purchase_quantity) ]
[docs] def adjust_stock(self, product_id, delta, created_by=None, type=None, *args, **kwargs): from shuup.core.suppliers.base import StockAdjustmentType adjustment_type = type or StockAdjustmentType.INVENTORY for module in self.modules: stock = module.adjust_stock(product_id, delta, created_by=created_by, type=adjustment_type, *args, **kwargs) if stock: return stock
[docs] def update_stock(self, product_id, *args, **kwargs): for module in self.modules: module.update_stock(product_id, *args, **kwargs)
[docs] def update_stocks(self, product_ids, *args, **kwargs): for module in self.modules: module.update_stocks(product_ids, *args, **kwargs)
[docs] def ship_products(self, shipment, product_quantities, *args, **kwargs): for module in self.modules: module.ship_products(shipment, product_quantities, *args, **kwargs)
[docs] def soft_delete(self): if not self.deleted: self.deleted = True self.save(update_fields=("deleted",))
class SupplierShop(models.Model): supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE, related_name="supplier_shops") shop = models.ForeignKey("shuup.Shop", on_delete=models.CASCADE, related_name="supplier_shops") is_approved = models.BooleanField( default=True, verbose_name=_("Approved"), help_text=_("Indicates whether this supplier is currently approved for work."), ) class Meta: unique_together = ("supplier", "shop") class SupplierModule(models.Model): module_identifier = models.CharField( max_length=64, verbose_name=_("module identifier"), unique=True, help_text=_( "Select the types of products this supplier can handle." "Example for normal products select just Simple Supplier." ), ) name = models.CharField( max_length=64, verbose_name=_("Module name"), help_text=_("Supplier modules name."), ) def __str__(self): return self.name @classmethod
[docs] def ensure_all_supplier_modules(cls): from shuup.apps.provides import get_provide_objects for module in get_provide_objects(Supplier.module_provides_key): cls.objects.update_or_create(module_identifier=module.identifier, defaults={"name": module.name})
SupplierLogEntry = define_log_model(Supplier)