Ejemplo n.º 1
0
class Product(TaxableItem, AttributableMixin, TranslatableModel):
    COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class")

    # Metadata
    created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on'))
    modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on'))
    deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_('deleted'))

    # Behavior
    mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_('mode'))
    variation_parent = models.ForeignKey(
        "self", null=True, blank=True, related_name='variation_children',
        on_delete=models.PROTECT,
        verbose_name=_('variation parent'))
    stock_behavior = EnumIntegerField(StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock'))
    shipping_mode = EnumIntegerField(ShippingMode, default=ShippingMode.NOT_SHIPPED, verbose_name=_('shipping mode'))
    sales_unit = models.ForeignKey("SalesUnit", verbose_name=_('unit'), blank=True, null=True, on_delete=models.PROTECT)
    tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'), on_delete=models.PROTECT)

    # Identification
    type = models.ForeignKey(
        "ProductType", related_name='products',
        on_delete=models.PROTECT, db_index=True,
        verbose_name=_('product type'))
    sku = models.CharField(db_index=True, max_length=128, verbose_name=_('SKU'), unique=True)
    gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_('Global Trade Item Number'))
    barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode'))
    accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('bookkeeping account'))
    profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True)
    cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True)
    # Category is duplicated here because not all products necessarily belong in Shops (i.e. have
    # ShopProduct instances), but they should nevertheless be searchable by category in other
    # places, such as administration UIs.
    category = models.ForeignKey(
        "Category", related_name='primary_products', blank=True, null=True,
        verbose_name=_('primary category'),
        help_text=_("only used for administration and reporting"), on_delete=models.PROTECT)

    # Physical dimensions
    width = MeasurementField(unit="mm", verbose_name=_('width (mm)'))
    height = MeasurementField(unit="mm", verbose_name=_('height (mm)'))
    depth = MeasurementField(unit="mm", verbose_name=_('depth (mm)'))
    net_weight = MeasurementField(unit="g", verbose_name=_('net weight (g)'))
    gross_weight = MeasurementField(unit="g", verbose_name=_('gross weight (g)'))

    # Misc.
    manufacturer = models.ForeignKey(
        "Manufacturer", blank=True, null=True,
        verbose_name=_('manufacturer'), on_delete=models.PROTECT)
    primary_image = models.ForeignKey(
        "ProductMedia", null=True, blank=True,
        related_name="primary_image_for_products",
        on_delete=models.SET_NULL,
        verbose_name=_("primary image"))

    translations = TranslatedFields(
        name=models.CharField(max_length=256, verbose_name=_('name')),
        description=models.TextField(blank=True, verbose_name=_('description')),
        slug=models.SlugField(verbose_name=_('slug'), max_length=255, null=True),
        keywords=models.TextField(blank=True, verbose_name=_('keywords')),
        status_text=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('status text'),
            help_text=_(
                'This text will be shown alongside the product in the shop.'
                ' (Ex.: "Available in a month")')),
        variation_name=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('variation name'))
    )

    objects = ProductQuerySet.as_manager()

    class Meta:
        ordering = ('-id',)
        verbose_name = _('product')
        verbose_name_plural = _('products')

    def __str__(self):
        try:
            return u"%s" % self.name
        except ObjectDoesNotExist:
            return self.sku

    def get_shop_instance(self, shop):
        """
        :type shop: shuup.core.models.Shop
        :rtype: shuup.core.models.ShopProduct
        """
        shop_inst_cache = self.__dict__.setdefault("_shop_inst_cache", {})
        cached = shop_inst_cache.get(shop)
        if cached:
            return cached

        shop_inst = self.shop_products.get(shop=shop)
        shop_inst._product_cache = self
        shop_inst._shop_cache = shop
        shop_inst_cache[shop] = shop_inst

        return shop_inst

    def get_priced_children(self, context, quantity=1):
        """
        Get child products with price infos sorted by price.

        :rtype: list[(Product,PriceInfo)]
        :return:
          List of products and their price infos sorted from cheapest to
          most expensive.
        """
        priced_children = (
            (child, child.get_price_info(context, quantity=quantity))
            for child in self.variation_children.all())
        return sorted(priced_children, key=(lambda x: x[1].price))

    def get_cheapest_child_price(self, context, quantity=1):
        price_info = self.get_cheapest_child_price_info(context, quantity)
        if price_info:
            return price_info.price

    def get_child_price_range(self, context, quantity=1):
        """
        Get the prices for cheapest and the most expensive child

        The attribute used for sorting is `PriceInfo.price`.

        Return (`None`, `None`) if `self.variation_children` do not exist.
        This is because we cannot return anything sensible.

        :type context: shuup.core.pricing.PricingContextable
        :type quantity: int
        :return: a tuple of prices
        :rtype: (shuup.core.pricing.Price, shuup.core.pricing.Price)
        """
        items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()]
        if not items:
            return (None, None)

        infos = sorted(items, key=lambda x: x.price)
        return (infos[0].price, infos[-1].price)

    def get_cheapest_child_price_info(self, context, quantity=1):
        """
        Get the `PriceInfo` of the cheapest variation child

        The attribute used for sorting is `PriceInfo.price`.

        Return `None` if `self.variation_children` do not exist.
        This is because we cannot return anything sensible.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.PriceInfo
        """
        items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()]
        if not items:
            return None

        return sorted(items, key=lambda x: x.price)[0]

    def get_price_info(self, context, quantity=1):
        """
        Get `PriceInfo` object for the product in given context.

        Returned `PriceInfo` object contains calculated `price` and
        `base_price`.  The calculation of prices is handled in the
        current pricing module.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.PriceInfo
        """
        from shuup.core.pricing import get_price_info
        return get_price_info(product=self, context=context, quantity=quantity)

    def get_price(self, context, quantity=1):
        """
        Get price of the product within given context.

        .. note::

           When the current pricing module implements pricing steps, it
           is possible that ``p.get_price(ctx) * 123`` is not equal to
           ``p.get_price(ctx, quantity=123)``, since there could be
           quantity discounts in effect, but usually they are equal.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.Price
        """
        return self.get_price_info(context, quantity).price

    def get_base_price(self, context, quantity=1):
        """
        Get base price of the product within given context.

        Base price differs from the (effective) price when there are
        discounts in effect.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.Price
        """
        return self.get_price_info(context, quantity=quantity).base_price

    def get_available_attribute_queryset(self):
        if self.type_id:
            return self.type.attributes.visible()
        else:
            return Attribute.objects.none()

    def get_available_variation_results(self):
        """
        Get a dict of `combination_hash` to product ID of variable variation results.

        :return: Mapping of combination hashes to product IDs
        :rtype: dict[str, int]
        """
        return dict(
            ProductVariationResult.objects.filter(product=self).filter(status=1)
            .values_list("combination_hash", "result_id")
        )

    def get_all_available_combinations(self):
        """
        Generate all available combinations of variation variables.

        If the product is not a variable variation parent, the iterator is empty.

        Because of possible combinatorial explosion this is a generator function.
        (For example 6 variables with 5 options each explodes to 15,625 combinations.)

        :return: Iterable of combination information dicts.
        :rtype: Iterable[dict]
        """
        return get_all_available_combinations(self)

    def clear_variation(self):
        """
        Fully remove variation information.

        Make this product a non-variation parent.
        """
        self.simplify_variation()
        for child in self.variation_children.all():
            if child.variation_parent_id == self.pk:
                child.unlink_from_parent()
        self.verify_mode()
        self.save()

    def simplify_variation(self):
        """
        Remove variation variables from the given variation parent, turning it
        into a simple variation (or a normal product, if it has no children).

        :param product: Variation parent to not be variable any longer.
        :type product: shuup.core.models.Product
        """
        ProductVariationVariable.objects.filter(product=self).delete()
        ProductVariationResult.objects.filter(product=self).delete()
        self.verify_mode()
        self.save()

    @staticmethod
    def _get_slug_name(self):
        if self.deleted:
            return None
        return (self.safe_translation_getter("name") or self.sku)

    def save(self, *args, **kwargs):
        if self.net_weight and self.net_weight > 0:
            self.gross_weight = max(self.net_weight, self.gross_weight)
        rv = super(Product, self).save(*args, **kwargs)
        generate_multilanguage_slugs(self, self._get_slug_name)
        return rv

    def delete(self, using=None):
        raise NotImplementedError("Not implemented: Use `soft_delete()` for products.")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user)
            # Bypassing local `save()` on purpose.
            super(Product, self).save(update_fields=("deleted",))

    def verify_mode(self):
        if ProductPackageLink.objects.filter(parent=self).exists():
            self.mode = ProductMode.PACKAGE_PARENT
            self.external_url = None
            self.variation_children.clear()
        elif ProductVariationVariable.objects.filter(product=self).exists():
            self.mode = ProductMode.VARIABLE_VARIATION_PARENT
        elif self.variation_children.exists():
            if ProductVariationResult.objects.filter(product=self).exists():
                self.mode = ProductMode.VARIABLE_VARIATION_PARENT
            else:
                self.mode = ProductMode.SIMPLE_VARIATION_PARENT
            self.external_url = None
            ProductPackageLink.objects.filter(parent=self).delete()
        elif self.variation_parent:
            self.mode = ProductMode.VARIATION_CHILD
            ProductPackageLink.objects.filter(parent=self).delete()
            self.variation_children.clear()
            self.external_url = None
        else:
            self.mode = ProductMode.NORMAL

    def unlink_from_parent(self):
        if self.variation_parent:
            parent = self.variation_parent
            self.variation_parent = None
            self.save()
            parent.verify_mode()
            self.verify_mode()
            self.save()
            ProductVariationResult.objects.filter(result=self).delete()
            return True

    def link_to_parent(self, parent, variables=None, combination_hash=None):
        """
        :param parent: The parent to link to.
        :type parent: Product
        :param variables: Optional dict of {variable identifier: value identifier} for complex variable linkage
        :type variables: dict|None
        :param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually
                                 exclusive with `variables`
        :type combination_hash: str|None

        """
        if combination_hash:
            if variables:
                raise ValueError("`combination_hash` and `variables` are mutually exclusive")
            variables = True  # Simplifies the below invariant checks

        self._raise_if_cant_link_to_parent(parent, variables)

        self.unlink_from_parent()
        self.variation_parent = parent
        self.verify_mode()
        self.save()
        if not parent.is_variation_parent():
            parent.verify_mode()
            parent.save()

        if variables:
            if not combination_hash:  # No precalculated hash, need to figure that out
                combination_hash = get_combination_hash_from_variable_mapping(parent, variables=variables)

            pvr = ProductVariationResult.objects.create(
                product=parent,
                combination_hash=combination_hash,
                result=self
            )
            if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT:
                parent.verify_mode()
                parent.save()
            return pvr
        else:
            return True

    def _raise_if_cant_link_to_parent(self, parent, variables):
        """
        Validates relation possibility for `self.link_to_parent()`

        :param parent: parent product of self
        :type parent: Product
        :param variables:
        :type variables: dict|None
        """
        if parent.is_variation_child():
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (parent is a child already)"),
                code="multilevel"
            )
        if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables:
            raise ImpossibleProductModeException(
                _("Parent is a variable variation parent, yet variables were not passed"),
                code="no_variables"
            )
        if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables:
            raise ImpossibleProductModeException(
                "Parent is a simple variation parent, yet variables were passed",
                code="extra_variables"
            )
        if self.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)"),
                code="multilevel"
            )
        if self.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)"),
                code="multilevel"
            )

    def make_package(self, package_def):
        if self.mode != ProductMode.NORMAL:
            raise ImpossibleProductModeException(
                _("Product is currently not a normal product, can't turn into package"),
                code="abnormal"
            )

        for child_product, quantity in six.iteritems(package_def):
            # :type child_product: Product
            if child_product.is_variation_parent():
                raise ImpossibleProductModeException(
                    _("Variation parents can not belong into a package"),
                    code="abnormal"
                )
            if child_product.is_package_parent():
                raise ImpossibleProductModeException(_("Packages can't be nested"), code="multilevel")
            if quantity <= 0:
                raise ImpossibleProductModeException(_("Quantity %s is invalid") % quantity, code="quantity")
            ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity)
        self.verify_mode()

    def get_package_child_to_quantity_map(self):
        if self.is_package_parent():
            product_id_to_quantity = dict(
                ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity")
            )
            products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys()))
            return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)}
        return {}

    def is_variation_parent(self):
        return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT)

    def is_variation_child(self):
        return (self.mode == ProductMode.VARIATION_CHILD)

    def get_variation_siblings(self):
        return Product.objects.filter(variation_parent=self.variation_parent).exclude(pk=self.pk)

    def is_package_parent(self):
        return (self.mode == ProductMode.PACKAGE_PARENT)

    def is_package_child(self):
        return ProductPackageLink.objects.filter(child=self).exists()

    def get_all_package_parents(self):
        return Product.objects.filter(pk__in=(
            ProductPackageLink.objects.filter(child=self).values_list("parent", flat=True)
        ))

    def get_all_package_children(self):
        return Product.objects.filter(pk__in=(
            ProductPackageLink.objects.filter(parent=self).values_list("child", flat=True)
        ))

    def get_public_media(self):
        return self.media.filter(enabled=True, public=True).exclude(kind=ProductMediaKind.IMAGE)

    def is_stocked(self):
        return (self.stock_behavior == StockBehavior.STOCKED)
Ejemplo n.º 2
0
class GDPRSettings(TranslatableModel):
    shop = models.OneToOneField("shuup.Shop",
                                related_name="gdpr_settings",
                                on_delete=models.CASCADE)
    enabled = models.BooleanField(default=False,
                                  verbose_name=_('enabled'),
                                  help_text=_("Define if the GDPR is active."))
    skip_consent_on_auth = models.BooleanField(
        default=False,
        verbose_name=_("skip consent on login"),
        help_text=_("Do not require consent on login when GDPR is activated."))
    privacy_policy_page = models.ForeignKey(
        on_delete=models.CASCADE,
        to="shuup_simple_cms.Page",
        null=True,
        verbose_name=_("privacy policy page"),
        help_text=_(
            "Choose your privacy policy page here. If this page changes, customers will be "
            "prompted for new consent."))
    consent_pages = models.ManyToManyField(
        "shuup_simple_cms.Page",
        verbose_name=_("consent pages"),
        related_name="consent_settings",
        help_text=_(
            "Choose pages here which are being monitored for customer consent. If any of these pages change, "
            "the customer is being prompted for a new consent."))
    translations = TranslatedFields(
        cookie_banner_content=models.TextField(
            blank=True,
            verbose_name=_("cookie banner content"),
            help_text=_(
                "The text to be presented to users in a pop-up warning.")),
        cookie_privacy_excerpt=models.TextField(
            blank=True,
            verbose_name=_("cookie privacy excerpt"),
            help_text=_(
                "The summary text to be presented about cookie privacy.")),
        auth_consent_text=models.TextField(
            blank=True,
            verbose_name=_("login consent text"),
            help_text=_(
                "Shown in login page between the form and the button. "
                "Optional, but should be considered when the consent on login is disabled."
            )))

    class Meta:
        verbose_name = _('GDPR settings')
        verbose_name_plural = _('GDPR settings')

    def __str__(self):
        return _("GDPR for {}").format(self.shop)

    def set_default_content(self):
        language = get_language()
        for code, name in settings.LANGUAGES:
            activate(code)
            self.set_current_language(code)
            self.cookie_banner_content = settings.SHUUP_GDPR_DEFAULT_BANNER_STRING
            self.cookie_privacy_excerpt = settings.SHUUP_GDPR_DEFAULT_EXCERPT_STRING
            self.save()

        self.set_current_language(language)
        activate(language)

    @classmethod
    def get_for_shop(cls, shop):
        instance, created = cls.objects.get_or_create(shop=shop)
        if created or not instance.safe_translation_getter(
                "cookie_banner_content"):
            instance.set_default_content()
        return instance
Ejemplo n.º 3
0
class DeliveryBoard(TranslatableModel):
    translations = TranslatedFields(delivery_comment=models.CharField(
        _("Comment"), max_length=50, blank=True, default=EMPTY_STRING))

    delivery_point = models.ForeignKey(
        "LUT_DeliveryPoint",
        verbose_name=_("Delivery point"),
        db_index=True,
        on_delete=models.PROTECT,
    )
    permanence = models.ForeignKey(
        "Permanence",
        verbose_name=REPANIER_SETTINGS_PERMANENCE_NAME,
        on_delete=models.CASCADE,
    )

    status = models.CharField(
        max_length=3,
        choices=LUT_PERMANENCE_STATUS,
        default=PERMANENCE_PLANNED,
        verbose_name=_("Status"),
    )
    is_updated_on = models.DateTimeField(_("Updated on"), auto_now=True)
    highest_status = models.CharField(
        max_length=3,
        choices=LUT_PERMANENCE_STATUS,
        default=PERMANENCE_PLANNED,
        verbose_name=_("Highest status"),
    )

    def set_status(self, new_status):
        from repanier.models.invoice import CustomerInvoice
        from repanier.models.purchase import PurchaseWoReceiver

        now = timezone.now()
        self.is_updated_on = now
        self.status = new_status
        if self.highest_status < new_status:
            self.highest_status = new_status
        self.save(update_fields=["status", "is_updated_on", "highest_status"])
        CustomerInvoice.objects.filter(
            delivery_id=self.id).order_by("?").update(status=new_status)
        PurchaseWoReceiver.objects.filter(
            customer_invoice__delivery_id=self.id).order_by("?").update(
                status=new_status)

    def get_delivery_display(self, br=False, color=False):
        short_name = "{}".format(
            self.delivery_point.safe_translation_getter("short_name",
                                                        any_language=True,
                                                        default=EMPTY_STRING))
        comment = self.safe_translation_getter("delivery_comment",
                                               any_language=True,
                                               default=EMPTY_STRING)
        if color:
            label = mark_safe('<font color="green">{} {}</font>'.format(
                comment, short_name))
        elif br:
            label = mark_safe("{}<br>{}".format(comment, short_name))
        else:
            label = "{} {}".format(comment, short_name)
        return label

    def get_delivery_status_display(self):
        return "{} - {}".format(self, self.get_status_display())

    def get_delivery_customer_display(self):
        if self.status != PERMANENCE_SEND:
            return "{} - {}".format(self, self.get_status_display())
        else:
            return "{} - {}".format(self, _("Orders closed"))

    def __str__(self):
        return self.get_delivery_display()

    class Meta:
        verbose_name = _("Delivery board")
        verbose_name_plural = _("Deliveries board")
        ordering = ("id", )
Ejemplo n.º 4
0
class Article(TranslatedAutoSlugifyMixin,
              TranslationHelperMixin,
              TranslatableModel):

    # TranslatedAutoSlugifyMixin options
    slug_source_field_name = 'title'
    slug_default = _('untitled-article')
    # when True, updates the article's search_data field
    # whenever the article is saved or a plugin is saved
    # on the article's content placeholder.
    update_search_on_save = getattr(
        settings,
        'ALDRYN_NEWSBLOG_UPDATE_SEARCH_DATA_ON_SAVE',
        False
    )

    translations = TranslatedFields(
        title=models.CharField(_('title'), max_length=234),
        slug=models.SlugField(
            verbose_name=_('slug'),
            max_length=255,
            db_index=True,
            blank=True,
            help_text=_(
                'Used in the URL. If changed, the URL will change. '
                'Clear it to have it re-created automatically.'),
        ),
        lead_in=HTMLField(
            verbose_name=_('lead'), default='',
            help_text=_(
                'The lead gives the reader the main idea of the story, this '
                'is useful in overviews, lists or as an introduction to your '
                'article.'
            ),
            blank=True,
        ),
        meta_title=models.CharField(
            max_length=255, verbose_name=_('meta title'),
            blank=True, default=''),
        meta_description=models.TextField(
            verbose_name=_('meta description'), blank=True, default=''),
        meta_keywords=models.TextField(
            verbose_name=_('meta keywords'), blank=True, default=''),
        meta={'unique_together': (('language_code', 'slug', ), )},

        search_data=models.TextField(blank=True, editable=False)
    )

    content = PlaceholderField('newsblog_article_content',
                               related_name='newsblog_article_content')
    author = models.ForeignKey(
        Person,
        null=True,
        blank=True,
        verbose_name=_('author'),
        on_delete=models.CASCADE,
    )
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('owner'),
        on_delete=models.CASCADE,
    )
    app_config = AppHookConfigField(
        NewsBlogConfig,
        verbose_name=_('Section'),
        help_text='',
    )
    categories = CategoryManyToManyField('aldryn_categories.Category',
                                         verbose_name=_('categories'),
                                         blank=True)
    publishing_date = models.DateTimeField(_('publishing date'),
                                           default=now)
    is_published = models.BooleanField(_('is published'), default=False,
                                       db_index=True)
    is_featured = models.BooleanField(_('is featured'), default=False,
                                      db_index=True)
    featured_image = FilerImageField(
        verbose_name=_('featured image'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    tags = TaggableManager(blank=True)

    # Setting "symmetrical" to False since it's a bit unexpected that if you
    # set "B relates to A" you immediately have also "A relates to B". It have
    # to be forced to False because by default it's True if rel.to is "self":
    #
    # https://github.com/django/django/blob/1.8.4/django/db/models/fields/related.py#L2144
    #
    # which in the end causes to add reversed releted-to entry as well:
    #
    # https://github.com/django/django/blob/1.8.4/django/db/models/fields/related.py#L977
    related = SortedManyToManyField('self', verbose_name=_('related articles'),
                                    blank=True, symmetrical=False)

    objects = RelatedManager()

    class Meta:
        ordering = ['-publishing_date']

    @property
    def published(self):
        """
        Returns True only if the article (is_published == True) AND has a
        published_date that has passed.
        """
        return self.is_published and self.publishing_date <= now()

    @property
    def future(self):
        """
        Returns True if the article is published but is scheduled for a
        future date/time.
        """
        return self.is_published and self.publishing_date > now()

    def get_absolute_url(self, language=None):
        """Returns the url for this Article in the selected permalink format."""
        if not language:
            language = get_current_language()
        kwargs = {}
        permalink_type = self.app_config.permalink_type
        if 'y' in permalink_type:
            kwargs.update(year=self.publishing_date.year)
        if 'm' in permalink_type:
            kwargs.update(month="%02d" % self.publishing_date.month)
        if 'd' in permalink_type:
            kwargs.update(day="%02d" % self.publishing_date.day)
        if 'i' in permalink_type:
            kwargs.update(pk=self.pk)
        if 's' in permalink_type:
            slug, lang = self.known_translation_getter(
                'slug', default=None, language_code=language)
            if slug and lang:
                site_id = getattr(settings, 'SITE_ID', None)
                if get_redirect_on_fallback(language, site_id):
                    language = lang
                kwargs.update(slug=slug)

        if self.app_config and self.app_config.namespace:
            namespace = '{0}:'.format(self.app_config.namespace)
        else:
            namespace = ''

        with override(language):
            return reverse('{0}article-detail'.format(namespace), kwargs=kwargs)

    def get_search_data(self, language=None, request=None):
        """
        Provides an index for use with Haystack, or, for populating
        Article.translations.search_data.
        """
        if not self.pk:
            return ''
        if language is None:
            language = get_current_language()
        if request is None:
            request = get_request(language=language)
        description = self.safe_translation_getter('lead_in', '')
        text_bits = [strip_tags(description)]
        for category in self.categories.all():
            text_bits.append(
                force_text(category.safe_translation_getter('name')))
        for tag in self.tags.all():
            text_bits.append(force_text(tag.name))
        if self.content:
            plugins = self.content.cmsplugin_set.filter(language=language)
            for base_plugin in plugins:
                plugin_text_content = ' '.join(
                    get_plugin_index_data(base_plugin, request))
                text_bits.append(plugin_text_content)
        return ' '.join(text_bits)

    def save(self, *args, **kwargs):
        # Update the search index
        if self.update_search_on_save:
            self.search_data = self.get_search_data()

        # Ensure there is an owner.
        if self.app_config.create_authors and self.author is None:
            self.author = Person.objects.get_or_create(
                user=self.owner,
                defaults={
                    'name': ' '.join((
                        self.owner.first_name,
                        self.owner.last_name,
                    )),
                })[0]
        # slug would be generated by TranslatedAutoSlugifyMixin
        super(Article, self).save(*args, **kwargs)

    def __str__(self):
        return self.safe_translation_getter('title', any_language=True)
Ejemplo n.º 5
0
class JobOpening(TranslatedAutoSlugifyMixin, TranslationHelperMixin,
                 TranslatableModel):
    slug_source_field_name = 'title'

    translations = TranslatedFields(
        title=models.CharField(_('title'), max_length=255),
        slug=models.SlugField(
            _('slug'),
            max_length=255,
            blank=True,
            unique=False,
            db_index=False,
            help_text=_('Auto-generated. Used in the URL. If changed, the URL '
                        'will change. Clear it to have the slug re-created.')),
        lead_in=HTMLField(
            _('short description'),
            blank=True,
            help_text=_('This text will be displayed in lists.')))

    content = PlaceholderField('Job Opening Content')
    category = models.ForeignKey(JobCategory,
                                 verbose_name=_('category'),
                                 related_name='jobs')
    created = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(_('active?'), default=True)
    publication_start = models.DateTimeField(_('published since'),
                                             null=True,
                                             blank=True)
    publication_end = models.DateTimeField(_('published until'),
                                           null=True,
                                           blank=True)
    can_apply = models.BooleanField(_('viewer can apply for the job?'),
                                    default=True)

    ordering = models.IntegerField(_('ordering'), default=0)

    objects = JobOpeningsManager()

    class Meta:
        verbose_name = _('job opening')
        verbose_name_plural = _('job openings')
        # DO NOT attempt to add 'translated__title' here.
        ordering = [
            'ordering',
        ]

    def __str__(self):
        return self.safe_translation_getter('title', str(self.pk))

    def _slug_exists(self, *args, **kwargs):
        """Provide additional filtering for slug generation"""
        qs = kwargs.get('qs', None)
        if qs is None:
            qs = self._get_slug_queryset()
        # limit qs to current app_config only
        kwargs['qs'] = qs.filter(category__app_config=self.category.app_config)
        return super(JobOpening, self)._slug_exists(*args, **kwargs)

    def get_absolute_url(self, language=None):
        language = language or self.get_current_language()
        slug = self.safe_translation_getter('slug', language_code=language)
        category_slug = self.category.safe_translation_getter(
            'slug', language_code=language)
        namespace = getattr(self.category.app_config, "namespace",
                            "aldryn_jobs")
        with force_language(language):
            try:
                # FIXME: does not looks correct return category url here
                if not slug:
                    return self.category.get_absolute_url(language=language)
                kwargs = {
                    'category_slug': category_slug,
                    'job_opening_slug': slug,
                }
                return reverse('{0}:job-opening-detail'.format(namespace),
                               kwargs=kwargs,
                               current_app=self.category.app_config.namespace)
            except NoReverseMatch:
                # FIXME: this is wrong, if have some problem in reverse
                #        we should know
                return "/%s/" % language

    def get_active(self):
        return all([
            self.is_active, self.publication_start is None
            or self.publication_start <= now(), self.publication_end is None
            or self.publication_end > now()
        ])

    def get_notification_emails(self):
        return self.category.get_notification_emails()
Ejemplo n.º 6
0
class Subnavigator(TranslatableModel):
    STATUS_CHOICES = (
        ('odinary', 'Odinary'),
        ('form', 'Form'),
        ('call', 'Call'),
        ('service', 'Service'),
        ('example', 'Example'),
        ('login', 'Login'),
    )
    translations = TranslatedFields(
        name=models.CharField(max_length=200,
                              verbose_name='Название раздела',
                              help_text='название основного раздела'),
        slug=models.SlugField(max_length=200,
                              null=True,
                              unique=True,
                              blank=True,
                              verbose_name='URL адрес',
                              help_text='название url адреса'),
        hreflogo=models.CharField(max_length=100,
                                  blank=True,
                                  verbose_name='URL картинка',
                                  help_text='URL картинка'),
        alt=models.CharField(max_length=150,
                             blank=True,
                             verbose_name='Alt картинка',
                             help_text='описание картинки'),
        title=models.CharField(
            max_length=300,
            blank=True,
            verbose_name='Заголовок',
            help_text='описание заголовка в строке браузера'),
        descrtionmeta=models.TextField(
            max_length=300,
            blank=True,
            verbose_name='Описание страницы',
            help_text='описание страницы в поискаовика'),
        keywordsmeta=models.TextField(
            max_length=300,
            blank=True,
            verbose_name='Ключевые слова',
            help_text='ключевые слова для поисковика'),
    )
    subname = models.ForeignKey('Navconstruct',
                                on_delete=models.CASCADE,
                                related_name="sub",
                                null=True,
                                blank=True,
                                verbose_name='Основное меню',
                                help_text='привязка к основному меню')
    template_name = models.ForeignKey('Templates',
                                      on_delete=models.CASCADE,
                                      related_name="subtemp",
                                      null=True,
                                      blank=True,
                                      verbose_name='Templates',
                                      help_text='привязка к Templates')
    pictures = models.ForeignKey(
        Pictures,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="subpict",
    )
    status = models.CharField(max_length=10,
                              choices=STATUS_CHOICES,
                              default='odinary')

    class Meta:
        verbose_name = "Подраздел"
        verbose_name_plural = "Подменю"

    def get_absolute_url(self):
        return reverse('standart:submain', args=[self.subname.slug, self.slug])

    def __str__(self):
        return self.name
Ejemplo n.º 7
0
class Post(ModelMeta, TranslatableModel):
    """
    Blog post
    """
    author = models.ForeignKey(dj_settings.AUTH_USER_MODEL,
                               verbose_name=_('author'),
                               null=True,
                               blank=True,
                               related_name='djangocms_blog_post_author')

    date_created = models.DateTimeField(_('created'), auto_now_add=True)
    date_modified = models.DateTimeField(_('last modified'), auto_now=True)
    date_published = models.DateTimeField(_('published since'),
                                          default=timezone.now)
    date_published_end = models.DateTimeField(_('published until'),
                                              null=True,
                                              blank=True)
    publish = models.BooleanField(_('publish'), default=False)
    categories = models.ManyToManyField('djangocms_blog.BlogCategory',
                                        verbose_name=_('category'),
                                        related_name='blog_posts',
                                        blank=True)
    main_image = FilerImageField(verbose_name=_('main image'),
                                 blank=True,
                                 null=True,
                                 on_delete=models.SET_NULL,
                                 related_name='djangocms_blog_post_image')
    main_image_thumbnail = models.ForeignKey(
        thumbnail_model,
        verbose_name=_('main image thumbnail'),
        related_name='djangocms_blog_post_thumbnail',
        on_delete=models.SET_NULL,
        blank=True,
        null=True)
    main_image_full = models.ForeignKey(
        thumbnail_model,
        verbose_name=_('main image full'),
        related_name='djangocms_blog_post_full',
        on_delete=models.SET_NULL,
        blank=True,
        null=True)
    enable_comments = models.BooleanField(
        verbose_name=_('enable comments on post'),
        default=get_setting('ENABLE_COMMENTS'))
    sites = models.ManyToManyField(
        'sites.Site',
        verbose_name=_('Site(s)'),
        blank=True,
        help_text=_('Select sites in which to show the post. '
                    'If none is set it will be '
                    'visible in all the configured sites.'))
    app_config = AppHookConfigField(BlogConfig,
                                    null=True,
                                    verbose_name=_('app. config'))

    translations = TranslatedFields(
        title=models.CharField(_('title'), max_length=255),
        slug=models.SlugField(_('slug'), blank=True, db_index=True),
        abstract=HTMLField(_('abstract'), blank=True, default=''),
        meta_description=models.TextField(
            verbose_name=_('post meta description'), blank=True, default=''),
        meta_keywords=models.TextField(verbose_name=_('post meta keywords'),
                                       blank=True,
                                       default=''),
        meta_title=models.CharField(
            verbose_name=_('post meta title'),
            help_text=_('used in title tag and social sharing'),
            max_length=255,
            blank=True,
            default=''),
        post_text=HTMLField(_('text'), default='', blank=True),
        meta={'unique_together': (('language_code', 'slug'), )})
    content = PlaceholderField('post_content', related_name='post_content')

    objects = GenericDateTaggedManager()
    tags = TaggableManager(blank=True, related_name='djangocms_blog_tags')

    _metadata = {
        'title': 'get_title',
        'description': 'get_description',
        'keywords': 'get_keywords',
        'og_description': 'get_description',
        'twitter_description': 'get_description',
        'gplus_description': 'get_description',
        'locale': 'get_locale',
        'image': 'get_image_full_url',
        'object_type': 'get_meta_attribute',
        'og_type': 'get_meta_attribute',
        'og_app_id': 'get_meta_attribute',
        'og_profile_id': 'get_meta_attribute',
        'og_publisher': 'get_meta_attribute',
        'og_author_url': 'get_meta_attribute',
        'og_author': 'get_meta_attribute',
        'twitter_type': 'get_meta_attribute',
        'twitter_site': 'get_meta_attribute',
        'twitter_author': 'get_meta_attribute',
        'gplus_type': 'get_meta_attribute',
        'gplus_author': 'get_meta_attribute',
        'published_time': 'date_published',
        'modified_time': 'date_modified',
        'expiration_time': 'date_published_end',
        'tag': 'get_tags',
        'url': 'get_absolute_url',
    }

    class Meta:
        verbose_name = _('blog article')
        verbose_name_plural = _('blog articles')
        ordering = ('-date_published', '-date_created')
        get_latest_by = 'date_published'

    def __str__(self):
        return self.safe_translation_getter('title')

    def get_absolute_url(self, lang=None):
        if not lang:
            lang = get_language()
        category = self.categories.first()
        kwargs = {}
        urlconf = get_setting('PERMALINK_URLS')[self.app_config.url_patterns]
        if '<year>' in urlconf:
            kwargs['year'] = self.date_published.year
        if '<month>' in urlconf:
            kwargs['month'] = '%02d' % self.date_published.month
        if '<day>' in urlconf:
            kwargs['day'] = '%02d' % self.date_published.day
        if '<slug>' in urlconf:
            kwargs['slug'] = self.safe_translation_getter(
                'slug', language_code=lang, any_language=True)  # NOQA
        if '<category>' in urlconf:
            kwargs['category'] = category.safe_translation_getter(
                'slug', language_code=lang, any_language=True)  # NOQA
        return reverse('%s:post-detail' % self.app_config.namespace,
                       kwargs=kwargs)

    def get_meta_attribute(self, param):
        """
        Retrieves django-meta attributes from apphook config instance
        :param param: django-meta attribute passed as key
        """
        attr = None
        value = getattr(self.app_config, param)
        if value:
            attr = getattr(self, value, None)
        if attr is not None:
            if callable(attr):
                try:
                    data = attr(param)
                except TypeError:
                    data = attr()
            else:
                data = attr
        else:
            data = value
        return data

    def save_translation(self, translation, *args, **kwargs):
        if not translation.slug and translation.title:
            translation.slug = slugify(translation.title)
        super(Post, self).save_translation(translation, *args, **kwargs)

    def get_title(self):
        title = self.safe_translation_getter('meta_title', any_language=True)
        if not title:
            title = self.safe_translation_getter('title', any_language=True)
        return title.strip()

    def get_keywords(self):
        return self.safe_translation_getter('meta_keywords',
                                            default='').strip().split(',')

    def get_locale(self):
        return self.get_current_language()

    def get_description(self):
        description = self.safe_translation_getter('meta_description',
                                                   any_language=True)
        if not description:
            description = self.safe_translation_getter('abstract',
                                                       any_language=True)
        return escape(strip_tags(description)).strip()

    def get_image_full_url(self):
        if self.main_image:
            return self.build_absolute_uri(self.main_image.url)
        return ''

    def get_tags(self):
        taglist = [tag.name for tag in self.tags.all()]
        return ','.join(taglist)

    def get_author(self):
        return self.author

    def _set_default_author(self, current_user):
        if not self.author_id and self.app_config.set_author:
            if get_setting('AUTHOR_DEFAULT') is True:
                user = current_user
            else:
                user = get_user_model().objects.get(
                    username=get_setting('AUTHOR_DEFAULT'))
            self.author = user

    def thumbnail_options(self):
        if self.main_image_thumbnail_id:
            return self.main_image_thumbnail.as_dict
        else:
            return get_setting('IMAGE_THUMBNAIL_SIZE')

    def full_image_options(self):
        if self.main_image_full_id:
            return self.main_image_full.as_dict
        else:
            return get_setting('IMAGE_FULL_SIZE')

    def get_full_url(self):
        return self.build_absolute_uri(self.get_absolute_url())
Ejemplo n.º 8
0
class ProxyModel(ProxyBase):
    proxy_translations = TranslatedFields(proxy_title=models.CharField(
        max_length=200))

    class Meta:
        proxy = True
Ejemplo n.º 9
0
class ForeignKeyTranslationModel(TranslatableModel):
    translations = TranslatedFields(translated_foreign=models.ForeignKey(
        "RegularModel", on_delete=models.CASCADE), )
    shared = models.CharField(max_length=200)
Ejemplo n.º 10
0
class Level2(Level1):
    l2_translations = TranslatedFields(l2_title=models.CharField(
        max_length=200))
Ejemplo n.º 11
0
class ProxyBase(TranslatableModel):
    base_translations = TranslatedFields(base_title=models.CharField(
        max_length=200))
Ejemplo n.º 12
0
class Level1(TranslatableModel):
    l1_translations = TranslatedFields(l1_title=models.CharField(
        max_length=200))
Ejemplo n.º 13
0
class ConcreteModel(AbstractModel):
    translations = TranslatedFields(
        tr_title=models.CharField("Translated Title", max_length=200))
Ejemplo n.º 14
0
class Page(MPTTModel, TranslatableModel):
    shop = models.ForeignKey("shuup.Shop", verbose_name=_('shop'))
    supplier = models.ForeignKey("shuup.Supplier", null=True, blank=True, verbose_name=_('supplier'))
    available_from = models.DateTimeField(
        default=now, null=True, blank=True, db_index=True,
        verbose_name=_('available from'), help_text=_(
            "Set an available from date to restrict the page to be available only after a certain date and time. "
            "This is useful for pages describing sales campaigns or other time-sensitive pages."
        )
    )
    available_to = models.DateTimeField(
        null=True, blank=True, db_index=True,
        verbose_name=_('available to'), help_text=_(
            "Set an available to date to restrict the page to be available only after a certain date and time. "
            "This is useful for pages describing sales campaigns or other time-sensitive pages."
        )
    )

    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL,
        verbose_name=_('created by')
    )
    modified_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL,
        verbose_name=_('modified by')
    )

    created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on'))
    modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on'))

    identifier = InternalIdentifierField(
        unique=False,
        help_text=_('This identifier can be used in templates to create URLs'),
        editable=True
    )

    visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=False, help_text=_(
        "Check this if this page should have a link in the top menu of the store front."
    ))
    parent = TreeForeignKey(
        "self", blank=True, null=True, related_name="children", verbose_name=_("parent"), help_text=_(
            "Set this to a parent page if this page should be subcategorized under another page."
        ))
    list_children_on_page = models.BooleanField(verbose_name=_("list children on page"), default=False, help_text=_(
        "Check this if this page should list its children pages."
    ))
    show_child_timestamps = models.BooleanField(verbose_name=_("show child page timestamps"), default=True, help_text=_(
        "Check this if you want to show timestamps on the child pages. Please note, that this "
        "requires the children to be listed on the page as well."
    ))
    deleted = models.BooleanField(default=False, verbose_name=_("deleted"))

    translations = TranslatedFields(
        title=models.CharField(max_length=256, verbose_name=_('title'), help_text=_(
            "The page title. This is shown anywhere links to your page are shown."
        )),
        url=models.CharField(
            max_length=100, verbose_name=_('URL'),
            default=None,
            blank=True,
            null=True,
            help_text=_(
                "The page url. Choose a descriptive url so that search engines can rank your page higher. "
                "Often the best url is simply the page title with spaces replaced with dashes."
            )
        ),
        content=models.TextField(verbose_name=_('content'), help_text=_(
            "The page content. This is the text that is displayed when customers click on your page link."
            "You can leave this empty and add all page content through placeholder editor in shop front."
            "To edit the style of the page you can use the Snippet plugin which is in shop front editor."
        ))
    )
    template_name = models.TextField(
        max_length=500,
        verbose_name=_("Template path"),
        default=settings.SHUUP_SIMPLE_CMS_DEFAULT_TEMPLATE
    )
    render_title = models.BooleanField(verbose_name=_("render title"), default=True, help_text=_(
        "Check this if this page should have a visible title"
    ))

    objects = TreeManager.from_queryset(PageQuerySet)()

    class Meta:
        ordering = ('-id',)
        verbose_name = _('page')
        verbose_name_plural = _('pages')
        unique_together = ("shop", "identifier")

    def delete(self, using=None):
        raise NotImplementedError("Not implemented: Use `soft_delete()`")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user)
            # Bypassing local `save()` on purpose.
            super(Page, self).save(update_fields=("deleted",))

    def clean(self):
        url = getattr(self, "url", None)
        if url:
            page_translation = self._meta.model._parler_meta.root_model
            shop_pages = Page.objects.for_shop(self.shop).values_list("id", flat=True)
            url_checker = page_translation.objects.filter(url=url, master_id__in=shop_pages)
            if self.pk:
                url_checker = url_checker.exclude(master_id=self.pk)
            if url_checker.exists():
                raise ValidationError(_("URL already exists."), code="invalid_url")

    def is_visible(self, dt=None):
        if not dt:
            dt = now()

        return (
            (self.available_from and self.available_from <= dt) and
            (self.available_to is None or self.available_to >= dt)
        )

    def save(self, *args, **kwargs):
        with reversion.create_revision():
            super(Page, self).save(*args, **kwargs)

    def get_html(self):
        return self.content

    @classmethod
    def create_initial_revision(cls, page):
        from reversion.models import Version
        if not Version.objects.get_for_object(page).exists():
            with reversion.create_revision():
                page.save()

    def __str__(self):
        return force_text(self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
Ejemplo n.º 15
0
class Staff(MPTTModel, TranslatableModel):
    parent = TreeForeignKey(
        "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE
    )
    customer_responsible = models.ForeignKey(
        "Customer",
        verbose_name=_("Customer responsible"),
        on_delete=models.PROTECT,
        null=True,
        default=None,
        blank=False,
    )
    login_attempt_counter = models.DecimalField(
        _("Login attempt counter"), default=DECIMAL_ZERO, max_digits=2, decimal_places=0
    )
    translations = TranslatedFields(
        long_name=models.CharField(
            _("Long name"),
            max_length=100,
            db_index=True,
            blank=True,
            default=EMPTY_STRING,
        ),
        function_description=HTMLField(
            _("Function description"),
            configuration="CKEDITOR_SETTINGS_MODEL2",
            blank=True,
            default=EMPTY_STRING,
        ),
    )

    is_repanier_admin = models.BooleanField(_("Repanier administrator"), default=False)
    is_order_manager = models.BooleanField(
        _("Offers in preparation manager"), default=False
    )
    is_invoice_manager = models.BooleanField(_("Billing offers manager"), default=False)
    is_webmaster = models.BooleanField(_("Webmaster"), default=False)
    is_other_manager = models.BooleanField(_("Other responsibility"), default=False)
    can_be_contacted = models.BooleanField(_("Can be contacted"), default=True)

    password_reset_on = models.DateTimeField(
        _("Password reset on"), null=True, blank=True, default=None
    )
    is_active = models.BooleanField(_("Active"), default=True)

    @classmethod
    def get_or_create_any_coordinator(cls):
        coordinator = (
            (
                cls.objects.filter(
                    is_active=True, is_repanier_admin=True, can_be_contacted=True
                )
                .order_by("id")
                .first()
            )
            or (
                cls.objects.filter(is_active=True, is_repanier_admin=True)
                .order_by("id")
                .first()
            )
            or (cls.objects.filter(is_active=True).order_by("id").first())
            or (cls.objects.order_by("id").first())
        )
        if coordinator is None:
            # Create the very first staff member
            from repanier.models.customer import Customer

            very_first_customer = Customer.get_or_create_the_very_first_customer()
            coordinator = Staff.objects.create(
                is_active=True,
                is_repanier_admin=True,
                is_webmaster=True,
                customer_responsible=very_first_customer,
                can_be_contacted=True,
            )
            cur_language = translation.get_language()
            for language in settings.PARLER_LANGUAGES[settings.SITE_ID]:
                language_code = language["code"]
                translation.activate(language_code)
                coordinator.set_current_language(language_code)
                coordinator.long_name = _("Coordinator")
                coordinator.save()
            translation.activate(cur_language)
        return coordinator

    @classmethod
    def get_or_create_order_responsible(cls):
        signature = []
        html_signature = []
        to_email = []
        order_responsible_qs = cls.objects.filter(
            is_active=True, is_order_manager=True
        ).order_by("?")
        if not order_responsible_qs.exists():
            order_responsible = Staff.get_or_create_any_coordinator()
            order_responsible.is_active = True
            order_responsible.is_order_manager = True
            order_responsible.save(update_fields=["is_active", "is_order_manager"])
        for order_responsible in order_responsible_qs:
            customer_responsible = order_responsible.customer_responsible
            can_be_contacted = order_responsible.can_be_contacted
            if customer_responsible is not None:
                if can_be_contacted:
                    signature.append(order_responsible.get_str_member)
                    html_signature.append(order_responsible.get_html_signature)
                to_email.extend(order_responsible.get_to_email)
        separator = chr(10) + " "
        return {
            "signature": separator.join(signature),
            "html_signature": mark_safe("<br>".join(html_signature)),
            "to_email": to_email,
        }

    @classmethod
    def get_or_create_invoice_responsible(cls):
        signature = []
        html_signature = []
        to_email = []
        invoice_responsible_qs = cls.objects.filter(
            is_active=True, is_invoice_manager=True
        ).order_by("?")
        if not invoice_responsible_qs.exists():
            invoice_responsible = Staff.get_or_create_any_coordinator()
            invoice_responsible.is_active = True
            invoice_responsible.is_order_manager = True
            invoice_responsible.save(update_fields=["is_active", "is_order_manager"])
        for invoice_responsible in invoice_responsible_qs:
            customer_responsible = invoice_responsible.customer_responsible
            can_be_contacted = invoice_responsible.can_be_contacted
            if customer_responsible is not None:
                if can_be_contacted:
                    signature.append(invoice_responsible.get_str_member)
                    html_signature.append(invoice_responsible.get_html_signature)
                to_email.extend(invoice_responsible.get_to_email)
        separator = chr(10) + " "
        return {
            "signature": separator.join(signature),
            "html_signature": mark_safe("<br>".join(html_signature)),
            "to_email": to_email,
        }

    @cached_property
    def get_html_signature(self):
        function_name = self.safe_translation_getter(
            "long_name", any_language=True, default=EMPTY_STRING
        )
        if self.customer_responsible is not None:
            customer = self.customer_responsible
            customer_name = customer.long_basket_name or customer.short_basket_name
            customer_contact_info = "{}{}".format(
                customer_name, customer.get_phone1(prefix=" - ")
            )
            html_signature = mark_safe(
                "{}<br>{}<br>{}".format(
                    customer_contact_info,
                    function_name,
                    settings.REPANIER_SETTINGS_GROUP_NAME,
                )
            )
        else:
            html_signature = mark_safe(
                "{}<br>{}".format(function_name, settings.REPANIER_SETTINGS_GROUP_NAME)
            )
        return html_signature

    @cached_property
    def get_to_email(self):
        if self.customer_responsible is not None:
            to_email = [self.customer_responsible.user.email]
        else:
            to_email = [settings.DEFAULT_FROM_EMAIL]
        return to_email

    @cached_property
    def get_str_member(self):
        if self.customer_responsible is not None:
            return "{} : {}{}".format(
                self,
                self.customer_responsible.long_basket_name or self.customer_responsible,
                self.customer_responsible.get_phone1(prefix=" (", postfix=")"),
            )
        else:
            return "{}".format(self)

    objects = StaffManager()

    def __str__(self):
        return self.safe_translation_getter(
            "long_name", any_language=True, default=EMPTY_STRING
        )

    class Meta:
        verbose_name = _("Staff member")
        verbose_name_plural = _("Staff members")
Ejemplo n.º 16
0
class ManyToManyOnlyFieldsTranslationModel(TranslatableModel):
    translations = TranslatedFields(
        translated_many_to_many=models.ManyToManyField("RegularModel"), )
    shared = models.CharField(max_length=200)
Ejemplo n.º 17
0
class Navconstruct(TranslatableModel):
    MENU_CHOICES = (
        ('first', 'First'),
        ('second', 'Second'),
    )
    STATUS_CHOICES = (
        ('odinary', 'Odinary'),
        ('form', 'Form'),
        ('call', 'Call'),
        ('service', 'Service'),
        ('example', 'Example'),
        ('login', 'Login'),
    )
    translations = TranslatedFields(
        name=models.CharField(max_length=200,
                              verbose_name='Название раздела',
                              help_text='название основного раздела'),
        slug=models.SlugField(max_length=200,
                              null=True,
                              unique=True,
                              blank=True,
                              verbose_name='URL адрес',
                              help_text='название url адреса'),
        hreflogo=models.TextField(max_length=100,
                                  blank=True,
                                  verbose_name='Текст блока',
                                  help_text='текст блока'),
        alt=models.TextField(max_length=300,
                             blank=True,
                             verbose_name='Текст блока',
                             help_text='текст блока'),
        title=models.CharField(
            max_length=300,
            blank=True,
            verbose_name='Заголовок',
            help_text='описание заголовка в строке браузера'),
        descrtionmeta=models.TextField(
            max_length=300,
            blank=True,
            verbose_name='Описание страницы',
            help_text='описание страницы в поискаовика'),
        keywordsmeta=models.TextField(
            max_length=300,
            blank=True,
            verbose_name='Ключевые слова',
            help_text='ключевые слова для поисковика'),
    )
    order = models.IntegerField(
        null=True,
        blank=True,
        verbose_name='Порядок в меню',
        help_text='последователность отображения в меню')
    template_name = models.ForeignKey('Templates',
                                      on_delete=models.CASCADE,
                                      related_name="navtemp",
                                      null=True,
                                      blank=True,
                                      verbose_name='Templates',
                                      help_text='привязка к Templates')
    example = models.BooleanField(verbose_name='Наличие примера',
                                  help_text='наличие примеров в разделе',
                                  default=False)
    pictures = models.ForeignKey(
        Pictures,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="navpict",
    )
    status = models.CharField(max_length=10,
                              choices=STATUS_CHOICES,
                              default='odinary')
    bar = models.CharField(max_length=10,
                           choices=MENU_CHOICES,
                           default='first')
    newslug = models.ForeignKey(
        Templatecategory,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="newslug",
        verbose_name='Первое меню',
    )
    newsslug = models.ForeignKey(
        Templatecategory,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="newsslug",
        verbose_name='Второе меню',
    )
    site = models.ManyToManyField(
        Site,
        blank=True,
        related_name="site",
    )

    class Meta:
        verbose_name = "Раздел"
        verbose_name_plural = "Основное меню"
        ordering = [
            "order",
        ]

    def get_absolute_url(self):
        if self.bar == 'first':
            return reverse('standart:navigator', args=[self.newslug])
        elif self.bar == 'second':
            return reverse('standart:submain',
                           args=[self.newslug, self.newsslug])

    def __str__(self):
        return self.name
Ejemplo n.º 18
0
class ManyToManyAndOtherFieldsTranslationModel(TranslatableModel):
    translations = TranslatedFields(
        tr_title=models.CharField("Translated Title", max_length=200),
        translated_many_to_many=models.ManyToManyField("RegularModel"),
    )
    shared = models.CharField(max_length=200)
Ejemplo n.º 19
0
class TeamMember(TranslatableModel):
    '''
    Model for members of the TEDxNTUA organizing team.

    The `team` attribute is represented as a CharField with limited possible
    values. The definition follows the official documentation example:
    https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
    '''
    EXPERIENCE = 'experience'
    IT = 'it'
    FUNDRAISING = 'fundraising'
    GRAPHICS = 'graphics'
    MEDIA = 'media'
    SPEAKERS = 'speakers'
    VENUE_PRODUCTION = 'venue-production'
    PHOTOGRAPHY = 'photography'
    TEAM_CHOICES = (
        (EXPERIENCE, 'Experience'),
        (IT, 'IT'),
        (FUNDRAISING, 'Fundraising'),
        (GRAPHICS, 'Graphics'),
        (MEDIA, 'Media'),
        (SPEAKERS, 'Speakers'),
        (VENUE_PRODUCTION, 'Venue & Production'),
        (PHOTOGRAPHY, 'Photography'),
    )
    translations = TranslatedFields(
        name=models.CharField(max_length=255, default=''))
    email = models.EmailField(null=True, blank=True)
    link = models.CharField(max_length=255, default='', null=True, blank=True)
    team = models.CharField(max_length=16, choices=TEAM_CHOICES)

    image = VersatileImageField(
        'Image 1',
        upload_to='team/',
        width_field='image_width',
        height_field='image_height',
        null=True,
        blank=True,
    )
    image_height = models.PositiveIntegerField(editable=False, null=True)
    image_width = models.PositiveIntegerField(editable=False, null=True)

    image_alt = VersatileImageField(
        'Image 2',
        upload_to='team/',
        width_field='image_alt_width',
        height_field='image_alt_height',
        null=True,
        blank=True,
    )
    image_alt_height = models.PositiveIntegerField(editable=False, null=True)
    image_alt_width = models.PositiveIntegerField(editable=False, null=True)

    is_published = models.BooleanField(_('Published'), default=True)

    objects = TeamMemberManager()

    def __str__(self):
        '''
        Objects of the TeamMember class are represented as strings by
        their fullname property
        '''
        return self.name
Ejemplo n.º 20
0
class IntegerPrimaryKeyModel(TranslatableModel):

    translations = TranslatedFields(
        tr_title=models.CharField("Translated Title", max_length=200))
Ejemplo n.º 21
0
class Attribute(TranslatableModel):
    identifier = InternalIdentifierField(unique=True, blank=False, null=False, editable=True)
    searchable = models.BooleanField(default=True, verbose_name=_("searchable"))
    type = EnumIntegerField(AttributeType, default=AttributeType.TRANSLATED_STRING, verbose_name=_("type"))
    visibility_mode = EnumIntegerField(
        AttributeVisibility,
        default=AttributeVisibility.SHOW_ON_PRODUCT_PAGE,
        verbose_name=_("visibility mode"))

    translations = TranslatedFields(
        name=models.CharField(max_length=64, verbose_name=_("name")),
    )

    objects = AttributeQuerySet.as_manager()

    class Meta:
        verbose_name = _('attribute')
        verbose_name_plural = _('attributes')

    def __str__(self):
        return u'%s' % self.name

    def save(self, *args, **kwargs):
        if not self.identifier:
            raise ValueError(u"Attribute with null identifier not allowed")
        self.identifier = flatten(("%s" % self.identifier).lower())
        return super(Attribute, self).save(*args, **kwargs)

    def formfield(self, **kwargs):
        """
        Get a form field for this attribute.

        :param kwargs: Kwargs to pass for the form field class.
        :return: Form field.
        :rtype: forms.Field
        """
        kwargs.setdefault("required", False)
        kwargs.setdefault("label", self.safe_translation_getter("name", self.identifier))
        if self.type == AttributeType.INTEGER:
            return forms.IntegerField(**kwargs)
        elif self.type == AttributeType.DECIMAL:
            return forms.DecimalField(**kwargs)
        elif self.type == AttributeType.BOOLEAN:
            return forms.NullBooleanField(**kwargs)
        elif self.type == AttributeType.TIMEDELTA:
            kwargs.setdefault("help_text", "(as seconds)")
            # TODO: This should be more user friendly
            return forms.DecimalField(**kwargs)
        elif self.type == AttributeType.DATETIME:
            return forms.DateTimeField(**kwargs)
        elif self.type == AttributeType.DATE:
            return forms.DateField(**kwargs)
        elif self.type == AttributeType.UNTRANSLATED_STRING:
            return forms.CharField(**kwargs)
        elif self.type == AttributeType.TRANSLATED_STRING:
            # Note: this isn't enough for actually saving multi-language entries;
            #       the caller will have to deal with calling this function several
            #       times for that.
            return forms.CharField(**kwargs)
        else:
            raise ValueError("`formfield` can't deal with fields of type %r" % self.type)

    @property
    def is_translated(self):
        return (self.type == AttributeType.TRANSLATED_STRING)

    @property
    def is_stringy(self):
        # Pun intended.
        return (self.type in ATTRIBUTE_STRING_TYPES)

    @property
    def is_numeric(self):
        return (self.type in ATTRIBUTE_NUMERIC_TYPES)

    @property
    def is_temporal(self):
        return (self.type in ATTRIBUTE_DATETIME_TYPES)

    def is_null_value(self, value):
        """
        Find out whether the given value is null from this attribute's point of view.

        :param value: A value
        :type value: object
        :return: Nulliness boolean
        :rtype: bool
        """
        if self.type == AttributeType.BOOLEAN:
            return (value is None)
        return (not value)
Ejemplo n.º 22
0
class UUIDPrimaryKeyModel(TranslatableModel):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    translations = TranslatedFields(
        tr_title=models.CharField("Translated Title", max_length=200))
Ejemplo n.º 23
0
class JobCategory(TranslatedAutoSlugifyMixin, TranslationHelperMixin,
                  TranslatableModel):
    slug_source_field_name = 'name'

    translations = TranslatedFields(
        name=models.CharField(_('name'), max_length=255),
        slug=models.SlugField(
            _('slug'),
            max_length=255,
            blank=True,
            help_text=_('Auto-generated. Used in the URL. If changed, the URL '
                        'will change. Clear it to have the slug re-created.')))

    supervisors = models.ManyToManyField(
        get_user_model_for_fields(),
        verbose_name=_('supervisors'),
        # FIXME: This is mis-named should be "job_categories"?
        related_name='job_opening_categories',
        help_text=_('Supervisors will be notified via email when a new '
                    'job application arrives.'),
        blank=True)
    app_config = models.ForeignKey(JobsConfig,
                                   null=True,
                                   verbose_name=_('app configuration'),
                                   related_name='categories')

    ordering = models.IntegerField(_('ordering'), default=0)

    objects = AppHookConfigTranslatableManager()

    class Meta:
        verbose_name = _('job category')
        verbose_name_plural = _('job categories')
        ordering = ['ordering']

    def __str__(self):
        return self.safe_translation_getter('name', str(self.pk))

    def _slug_exists(self, *args, **kwargs):
        """Provide additional filtering for slug generation"""
        qs = kwargs.get('qs', None)
        if qs is None:
            qs = self._get_slug_queryset()
        # limit qs to current app_config only
        kwargs['qs'] = qs.filter(app_config=self.app_config)
        return super(JobCategory, self)._slug_exists(*args, **kwargs)

    def get_absolute_url(self, language=None):
        language = language or self.get_current_language()
        slug = self.safe_translation_getter('slug', language_code=language)
        if self.app_config_id:
            namespace = self.app_config.namespace
        else:
            namespace = 'aldryn_jobs'
        with force_language(language):
            try:
                if not slug:
                    return reverse('{0}:job-opening-list'.format(namespace))
                kwargs = {'category_slug': slug}
                return reverse(
                    '{0}:category-job-opening-list'.format(namespace),
                    kwargs=kwargs,
                    current_app=self.app_config.namespace)
            except NoReverseMatch:
                return "/%s/" % language

    def get_notification_emails(self):
        return self.supervisors.values_list('email', flat=True)

    # We keep this 'count' name for compatibility in templates:
    # there used to be annotate() call with the same property name.
    def count(self):
        return self.jobs.active().count()
Ejemplo n.º 24
0
class NewsBlogConfig(TranslatableModel, AppHookConfig):
    """Adds some translatable, per-app-instance fields."""
    translations = TranslatedFields(app_title=models.CharField(
        _('application title'), max_length=234), )

    permalink_type = models.CharField(
        _('permalink type'),
        max_length=8,
        blank=False,
        default='slug',
        choices=PERMALINK_CHOICES,
        help_text=_('Choose the style of urls to use from the examples. '
                    '(Note, all types are relative to apphook)'))

    non_permalink_handling = models.SmallIntegerField(
        _('non-permalink handling'),
        blank=False,
        default=302,
        choices=NON_PERMALINK_HANDLING,
        help_text=_('How to handle non-permalink urls?'))

    paginate_by = models.PositiveIntegerField(
        _('Paginate size'),
        blank=False,
        default=5,
        help_text=_('When paginating list views, how many articles per page?'),
    )
    pagination_pages_start = models.PositiveIntegerField(
        _('Pagination pages start'),
        blank=False,
        default=10,
        help_text=_('When paginating list views, after how many pages '
                    'should we start grouping the page numbers.'),
    )
    pagination_pages_visible = models.PositiveIntegerField(
        _('Pagination pages visible'),
        blank=False,
        default=4,
        help_text=_('When grouping page numbers, this determines how many '
                    'pages are visible on each side of the active page.'),
    )

    template_prefix = models.CharField(
        max_length=20,
        null=True,
        blank=True,
        choices=TEMPLATE_PREFIX_CHOICES,
        verbose_name=_("Prefix for template dirs"))

    # ALDRYN_NEWSBLOG_CREATE_AUTHOR
    create_authors = models.BooleanField(
        _('Auto-create authors?'),
        default=True,
        help_text=_('Automatically create authors from logged-in user?'),
    )

    # ALDRYN_NEWSBLOG_SEARCH
    search_indexed = models.BooleanField(
        _('Include in search index?'),
        default=True,
        help_text=_('Include articles in search indexes?'),
    )

    placeholder_base_top = PlaceholderField(
        'newsblog_base_top',
        related_name='aldryn_newsblog_base_top',
    )

    placeholder_base_sidebar = PlaceholderField(
        'newsblog_base_sidebar',
        related_name='aldryn_newsblog_base_sidebar',
    )

    placeholder_list_top = PlaceholderField(
        'newsblog_list_top',
        related_name='aldryn_newsblog_list_top',
    )

    placeholder_list_footer = PlaceholderField(
        'newsblog_list_footer',
        related_name='aldryn_newsblog_list_footer',
    )

    placeholder_detail_top = PlaceholderField(
        'newsblog_detail_top',
        related_name='aldryn_newsblog_detail_top',
    )

    placeholder_detail_bottom = PlaceholderField(
        'newsblog_detail_bottom',
        related_name='aldryn_newsblog_detail_bottom',
    )

    placeholder_detail_footer = PlaceholderField(
        'newsblog_detail_footer',
        related_name='aldryn_newsblog_detail_footer',
    )

    def get_app_title(self):
        return getattr(self, 'app_title', _('untitled'))

    class Meta:
        verbose_name = 'config'
        verbose_name_plural = 'configs'
Ejemplo n.º 25
0
class Slide(TranslatableWshopModel):
    carousel = models.ForeignKey(Carousel, related_name="slides", on_delete=models.CASCADE)
    name = models.CharField(
        max_length=50, blank=True, null=True, verbose_name=_("name"),
        help_text=_("Name is only used to configure slides.")
    )
    product_link = models.ForeignKey(
        Product, related_name="+", blank=True, null=True, verbose_name=_("product link"), help_text=_(
            "Set the product detail page that should be shown when this slide is clicked, if any."
        ))
    category_link = models.ForeignKey(
        Category, related_name="+", blank=True, null=True, verbose_name=_("category link"), help_text=_(
            "Set the product category page that should be shown when this slide is clicked, if any."
        ))
    cms_page_link = models.ForeignKey(
        Page, related_name="+", verbose_name=_("cms page link"), blank=True, null=True, help_text=_(
            "Set the web page that should be shown when the slide is clicked, if any."
        ))
    ordering = models.IntegerField(default=0, blank=True, null=True, verbose_name=_("ordering"), help_text=_(
        "Set the numeric order in which this slide should appear relative to other slides in this carousel."
    ))
    target = EnumIntegerField(
        LinkTargetType, default=LinkTargetType.CURRENT, verbose_name=_("link target"), help_text=_(
            "Set this to current if clicking on this slide should open a new browser tab."
        )
    )
    available_from = models.DateTimeField(null=True, blank=True, verbose_name=_('available from'), help_text=_(
        "Set the date and time from which this slide should be visible in the carousel. "
        "This is useful to advertise sales campaigns or other time-sensitive marketing."
    ))
    available_to = models.DateTimeField(null=True, blank=True, verbose_name=_('available to'), help_text=_(
        "Set the date and time from which this slide should be visible in the carousel. "
        "This is useful to advertise sales campaigns or other time-sensitive marketing."
    ))

    translations = TranslatedFields(
        caption=models.CharField(
            max_length=80, blank=True, null=True, verbose_name=_("caption"), help_text=_(
                "Text that describes the image. Used for search engine purposes."
            )
        ),
        caption_text=models.TextField(
            blank=True, null=True, verbose_name=_("caption text"),
            help_text=_("When displayed in banner box mode, caption text is shown as a tooltip"),
        ),
        external_link=models.CharField(
            max_length=160, blank=True, null=True, verbose_name=_("external link"), help_text=_(
                "Set the external site that should be shown when this slide is clicked, if any."
            )),
        image=FilerImageField(
            blank=True, null=True, related_name="+", verbose_name=_("image"), on_delete=models.PROTECT, help_text=_(
                "The slide image to show."
            ))
    )

    def __str__(self):
        return "%s %s" % (_("Slide"), self.pk)

    class Meta:
        verbose_name = _("Slide")
        verbose_name_plural = _("Slides")
        ordering = ("ordering", "id")

    def get_translated_field(self, attr):
        if not self.safe_translation_getter(attr):
            return self.safe_translation_getter(attr, language_code=settings.PARLER_DEFAULT_LANGUAGE_CODE)
        return getattr(self, attr)

    def get_link_url(self):
        """
        Get right link url for this slide.

        Initially external link is used. If not set link will fallback to
        product_link, external_link or cms_page_link in this order.

        :return: return correct link url for slide if set
        :rtype: str|None
        """
        external_link = self.get_translated_field("external_link")
        if external_link:
            return external_link
        elif self.product_link:
            return reverse("wshop:product", kwargs=dict(pk=self.product_link.pk, slug=self.product_link.slug))
        elif self.category_link:
            return reverse("wshop:category", kwargs=dict(pk=self.category_link.pk, slug=self.category_link.slug))
        elif self.cms_page_link:
            return reverse("wshop:cms_page", kwargs=dict(url=self.cms_page_link.url))

    def is_visible(self, dt=None):
        """
        Get slides that should be publicly visible.

        This does not do permission checking.

        :param dt: Datetime for visibility check
        :type dt: datetime.datetime
        :return: Public visibility status
        :rtype: bool
        """
        if not dt:
            dt = now()

        return (
            (self.available_from and self.available_from <= dt) and
            (self.available_to is None or self.available_to >= dt)
        )

    def get_link_target(self):
        """
        Return link target type string based on selection

        :return: Target type string
        :rtype: str
        """
        if self.target == LinkTargetType.NEW:
            return "_blank"
        else:
            return "_self"

    @property
    def easy_thumbnails_thumbnailer(self):
        """
        Get Thumbnailer instance for the translated image.
        Will return None if file cannot be thumbnailed.
        :rtype:easy_thumbnails.files.Thumbnailer|None
        """
        image = self.get_translated_field("image")

        if not image:
            return

        try:
            return get_thumbnailer(image)
        except ValueError:
            return get_thumbnailer(image.filer_image_file)
        except:
            return None

    def get_thumbnail(self, **kwargs):
        """
        Get thumbnail for the translated image
        This will return None if there is no file
        :rtype: easy_thumbnails.files.ThumbnailFile|None
        """
        kwargs.setdefault("size", (self.carousel.image_width, self.carousel.image_height))
        kwargs.setdefault("crop", True)  # sane defaults
        kwargs.setdefault("upscale", True)  # sane defaults

        if kwargs["size"] is (0, 0):
            return None

        thumbnailer = self.easy_thumbnails_thumbnailer

        if not thumbnailer:
            return None

        return thumbnailer.get_thumbnail(thumbnail_options=kwargs)

    objects = SlideQuerySet.as_manager()
Ejemplo n.º 26
0
class OrderStatus(TranslatableModel):
    identifier = InternalIdentifierField(
        db_index=True,
        blank=False,
        editable=True,
        unique=True,
        help_text=
        _("Internal identifier for status. This is used to identify the statuses in Shuup."
          ))
    ordering = models.IntegerField(
        db_index=True,
        default=0,
        verbose_name=_('ordering'),
        help_text=
        _("The processing order of statuses. Default is always processed first."
          ))
    role = EnumIntegerField(
        OrderStatusRole,
        db_index=True,
        default=OrderStatusRole.NONE,
        verbose_name=_('role'),
        help_text=_(
            "Role of status. One role can have multiple order statuses."))
    default = models.BooleanField(
        default=False,
        db_index=True,
        verbose_name=_('default'),
        help_text=_("Defines if the status should be considered as default."))

    is_active = models.BooleanField(
        default=True,
        db_index=True,
        verbose_name=_('is active'),
        help_text=_("Define if the status is usable."))

    objects = OrderStatusQuerySet.as_manager()

    translations = TranslatedFields(
        name=models.CharField(verbose_name=_("name"),
                              max_length=64,
                              help_text=_("Name of the order status")),
        public_name=models.CharField(
            verbose_name=_('public name'),
            max_length=64,
            help_text=_("The name shown for customer in shop front.")))

    class Meta:
        unique_together = ("identifier", "role")
        verbose_name = _('order status')
        verbose_name_plural = _('order statuses')

    def __str__(self):
        return force_text(
            self.safe_translation_getter("name", default=self.identifier))

    def save(self, *args, **kwargs):
        super(OrderStatus, self).save(*args, **kwargs)
        if self.default and self.role != OrderStatusRole.NONE:
            # If this status is the default, make the others for this role non-default.
            OrderStatus.objects.filter(role=self.role).exclude(
                pk=self.pk).update(default=False)
Ejemplo n.º 27
0
class Product(TaxableItem, AttributableMixin, TranslatableModel):
    COMMON_SELECT_RELATED = ("type", "primary_image", "tax_class")

    # Metadata
    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, verbose_name=_('modified on'))
    deleted = models.BooleanField(default=False, editable=False, db_index=True, verbose_name=_('deleted'))

    # Behavior
    mode = EnumIntegerField(ProductMode, default=ProductMode.NORMAL, verbose_name=_('mode'))
    variation_parent = models.ForeignKey(
        "self", null=True, blank=True, related_name='variation_children',
        on_delete=models.PROTECT,
        verbose_name=_('variation parent'))
    stock_behavior = EnumIntegerField(
        StockBehavior, default=StockBehavior.UNSTOCKED, verbose_name=_('stock'),
        help_text=_("Set to stocked if inventory should be managed within Shuup.")
    )
    shipping_mode = EnumIntegerField(
        ShippingMode, default=ShippingMode.SHIPPED, verbose_name=_('shipping mode'),
        help_text=_("Set to shipped if the product requires shipment.")
    )
    sales_unit = models.ForeignKey(
        "SalesUnit", verbose_name=_('sales unit'), blank=True, null=True, on_delete=models.PROTECT, help_text=_(
            "Select a sales unit for your product. "
            "This is shown in your store front and is used to determine whether the product can be purchased using "
            "fractional amounts. Sales units are defined in Products - Sales Units."
        )
    )
    tax_class = models.ForeignKey("TaxClass", verbose_name=_('tax class'), on_delete=models.PROTECT, help_text=_(
            "Select a tax class for your product. "
            "The tax class is used to determine which taxes to apply to your product. "
            "Tax classes are defined in Settings - Tax Classes. "
            "The rules by which taxes are applied are defined in Settings - Tax Rules."
        )
    )

    # Identification
    type = models.ForeignKey(
        "ProductType", related_name='products',
        on_delete=models.PROTECT, db_index=True,
        verbose_name=_('product type'),
        help_text=_(
            "Select a product type for your product. "
            "These allow you to configure custom attributes to help with classification and analysis."
        )
    )
    sku = models.CharField(
        db_index=True, max_length=128, verbose_name=_('SKU'), unique=True,
        help_text=_(
            "Enter a SKU (Stock Keeping Unit) number for your product. "
            "This is a product identification code that helps you track it through your inventory. "
            "People often use the number by the barcode on the product, "
            "but you can set up any numerical system you want to keep track of products."
        )
    )
    gtin = models.CharField(blank=True, max_length=40, verbose_name=_('GTIN'), help_text=_(
        "You can enter a Global Trade Item Number. "
        "This is typically a 14 digit identification number for all of your trade items. "
        "It can often be found by the barcode."
    ))
    barcode = models.CharField(blank=True, max_length=40, verbose_name=_('barcode'), help_text=_(
        "You can enter the barcode number for your product. This is useful for inventory/stock tracking and analysis."
    ))
    accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('bookkeeping account'))
    profit_center = models.CharField(max_length=32, verbose_name=_('profit center'), blank=True)
    cost_center = models.CharField(max_length=32, verbose_name=_('cost center'), blank=True)

    # Physical dimensions
    width = MeasurementField(
        unit="mm", verbose_name=_('width (mm)'),
        help_text=_(
            "Set the measured width of your product or product packaging. "
            "This will provide customers with your product size and help with calculating shipping costs."
        )
    )
    height = MeasurementField(
        unit="mm", verbose_name=_('height (mm)'),
        help_text=_(
            "Set the measured height of your product or product packaging. "
            "This will provide customers with your product size and help with calculating shipping costs."
        )
    )
    depth = MeasurementField(
        unit="mm", verbose_name=_('depth (mm)'),
        help_text=_(
            "Set the measured depth or length of your product or product packaging. "
            "This will provide customers with your product size and help with calculating shipping costs."
        )
    )
    net_weight = MeasurementField(
        unit="g", verbose_name=_('net weight (g)'),
        help_text=_(
            "Set the measured weight of your product WITHOUT its packaging. "
            "This will provide customers with your product weight."
        )
    )
    gross_weight = MeasurementField(
        unit="g", verbose_name=_('gross weight (g)'),
        help_text=_(
            "Set the measured gross Weight of your product WITH its packaging. "
            "This will help with calculating shipping costs."
        )
    )

    # Misc.
    manufacturer = models.ForeignKey(
        "Manufacturer", blank=True, null=True,
        verbose_name=_('manufacturer'), on_delete=models.PROTECT, help_text=_(
            "Select a manufacturer for your product. These are defined in Products Settings - Manufacturers"
        )
    )
    primary_image = models.ForeignKey(
        "ProductMedia", null=True, blank=True,
        related_name="primary_image_for_products",
        on_delete=models.SET_NULL,
        verbose_name=_("primary image"))

    translations = TranslatedFields(
        name=models.CharField(
            max_length=256, verbose_name=_('name'),
            help_text=_("Enter a descriptive name for your product. This will be its title in your store.")),
        description=models.TextField(
            blank=True, verbose_name=_('description'),
            help_text=_(
                "To make your product stand out, give it an awesome description. "
                "This is what will help your shoppers learn about your products. "
                "It will also help shoppers find them in the store and on the web."
            )
        ),
        short_description=models.CharField(
            max_length=150, blank=True, verbose_name=_('short description'),
            help_text=_(
                "Enter a short description for your product. "
                "The short description will be used to get the attention of your "
                "customer with a small but precise description of your product."
            )
        ),
        slug=models.SlugField(
            verbose_name=_('slug'), max_length=255, blank=True, null=True,
            help_text=_(
                "Enter a URL Slug for your product. This is what your product page URL will be. "
                "A default will be created using the product name."
            )
        ),
        keywords=models.TextField(blank=True, verbose_name=_('keywords'), help_text=_(
                "You can enter keywords that describe your product. "
                "This will help your shoppers learn about your products. "
                "It will also help shoppers find them in the store and on the web."
            )
        ),
        status_text=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('status text'),
            help_text=_(
                'This text will be shown alongside the product in the shop. '
                'It is useful for informing customers of special stock numbers or preorders. '
                '(Ex.: "Available in a month")'
            )
        ),
        variation_name=models.CharField(
            max_length=128, blank=True,
            verbose_name=_('variation name'),
            help_text=_(
                "You can enter a name for the variation of your product. "
                "This could be for example different colors or versions."
            )
        )
    )

    objects = ProductQuerySet.as_manager()

    class Meta:
        ordering = ('-id',)
        verbose_name = _('product')
        verbose_name_plural = _('products')

    def __str__(self):
        try:
            return u"%s" % self.name
        except ObjectDoesNotExist:
            return self.sku

    def get_shop_instance(self, shop, allow_cache=False):
        """
        :type shop: shuup.core.models.Shop
        :rtype: shuup.core.models.ShopProduct
        """

        # FIXME: Temporary removed the cache to prevent parler issues
        # Uncomment this as soon as https://github.com/shuup/shuup/issues/1323 is fixed
        # and Django Parler version is bumped with the fix

        # from shuup.core.utils import context_cache
        # key, val = context_cache.get_cached_value(
        #     identifier="shop_product", item=self, context={"shop": shop}, allow_cache=allow_cache)
        # if val is not None:
        #     return val
        shop_inst = self.shop_products.get(shop_id=shop.id)
        # context_cache.set_cached_value(key, shop_inst)
        return shop_inst

    def get_priced_children(self, context, quantity=1):
        """
        Get child products with price infos sorted by price.

        :rtype: list[(Product,PriceInfo)]
        :return:
          List of products and their price infos sorted from cheapest to
          most expensive.
        """
        from shuup.core.models import ShopProduct
        priced_children = []
        shop_product_query = Q(
            shop=context.shop,
            product_id__in=self.variation_children.all().values_list("id", flat=True)
        )
        for shop_product in ShopProduct.objects.filter(shop_product_query):
            if shop_product.is_orderable(supplier=None, customer=context.customer, quantity=1):
                child = shop_product.product
                priced_children.append((child, child.get_price_info(context, quantity=quantity)))

        return sorted(priced_children, key=(lambda x: x[1].price))

    def get_cheapest_child_price(self, context, quantity=1):
        price_info = self.get_cheapest_child_price_info(context, quantity)
        if price_info:
            return price_info.price

    def get_child_price_range(self, context, quantity=1):
        """
        Get the prices for cheapest and the most expensive child

        The attribute used for sorting is `PriceInfo.price`.

        Return (`None`, `None`) if `self.variation_children` do not exist.
        This is because we cannot return anything sensible.

        :type context: shuup.core.pricing.PricingContextable
        :type quantity: int
        :return: a tuple of prices
        :rtype: (shuup.core.pricing.Price, shuup.core.pricing.Price)
        """
        items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()]
        if not items:
            return (None, None)

        infos = sorted(items, key=lambda x: x.price)
        return (infos[0].price, infos[-1].price)

    def get_cheapest_child_price_info(self, context, quantity=1):
        """
        Get the `PriceInfo` of the cheapest variation child

        The attribute used for sorting is `PriceInfo.price`.

        Return `None` if `self.variation_children` do not exist.
        This is because we cannot return anything sensible.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.PriceInfo
        """
        items = [c.get_price_info(context, quantity=quantity) for c in self.variation_children.all()]
        if not items:
            return None

        return sorted(items, key=lambda x: x.price)[0]

    def get_price_info(self, context, quantity=1):
        """
        Get `PriceInfo` object for the product in given context.

        Returned `PriceInfo` object contains calculated `price` and
        `base_price`.  The calculation of prices is handled in the
        current pricing module.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.PriceInfo
        """
        from shuup.core.pricing import get_price_info
        return get_price_info(product=self, context=context, quantity=quantity)

    def get_price(self, context, quantity=1):
        """
        Get price of the product within given context.

        .. note::

           When the current pricing module implements pricing steps, it
           is possible that ``p.get_price(ctx) * 123`` is not equal to
           ``p.get_price(ctx, quantity=123)``, since there could be
           quantity discounts in effect, but usually they are equal.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.Price
        """
        return self.get_price_info(context, quantity).price

    def get_base_price(self, context, quantity=1):
        """
        Get base price of the product within given context.

        Base price differs from the (effective) price when there are
        discounts in effect.

        :type context: shuup.core.pricing.PricingContextable
        :rtype: shuup.core.pricing.Price
        """
        return self.get_price_info(context, quantity=quantity).base_price

    def get_available_attribute_queryset(self):
        if self.type_id:
            return self.type.attributes.visible()
        else:
            return Attribute.objects.none()

    def get_available_variation_results(self):
        """
        Get a dict of `combination_hash` to product ID of variable variation results.

        :return: Mapping of combination hashes to product IDs
        :rtype: dict[str, int]
        """
        return dict(
            ProductVariationResult.objects.filter(product=self).filter(status=1)
            .values_list("combination_hash", "result_id")
        )

    def get_all_available_combinations(self):
        """
        Generate all available combinations of variation variables.

        If the product is not a variable variation parent, the iterator is empty.

        Because of possible combinatorial explosion this is a generator function.
        (For example 6 variables with 5 options each explodes to 15,625 combinations.)

        :return: Iterable of combination information dicts.
        :rtype: Iterable[dict]
        """
        return get_all_available_combinations(self)

    def clear_variation(self):
        """
        Fully remove variation information.

        Make this product a non-variation parent.
        """
        self.simplify_variation()
        for child in self.variation_children.all():
            if child.variation_parent_id == self.pk:
                child.unlink_from_parent()
        self.verify_mode()
        self.save()

    def simplify_variation(self):
        """
        Remove variation variables from the given variation parent, turning it
        into a simple variation (or a normal product, if it has no children).

        :param product: Variation parent to not be variable any longer.
        :type product: shuup.core.models.Product
        """
        ProductVariationVariable.objects.filter(product=self).delete()
        ProductVariationResult.objects.filter(product=self).delete()
        self.verify_mode()
        self.save()

    @staticmethod
    def _get_slug_name(self, translation=None):
        if self.deleted:
            return None
        return getattr(translation, "name", self.sku)

    def save(self, *args, **kwargs):
        self.clean()
        if self.net_weight and self.net_weight > 0:
            self.gross_weight = max(self.net_weight, self.gross_weight)
        rv = super(Product, self).save(*args, **kwargs)
        generate_multilanguage_slugs(self, self._get_slug_name)
        return rv

    def clean(self):
        pre_clean.send(type(self), instance=self)
        super(Product, self).clean()
        post_clean.send(type(self), instance=self)

    def delete(self, using=None):
        raise NotImplementedError("Not implemented: Use `soft_delete()` for products.")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user)
            # Bypassing local `save()` on purpose.
            super(Product, self).save(update_fields=("deleted",))

    def verify_mode(self):
        if ProductPackageLink.objects.filter(parent=self).exists():
            self.mode = ProductMode.PACKAGE_PARENT
            self.external_url = None
            self.variation_children.clear()
        elif ProductVariationVariable.objects.filter(product=self).exists():
            self.mode = ProductMode.VARIABLE_VARIATION_PARENT
        elif self.variation_children.exists():
            if ProductVariationResult.objects.filter(product=self).exists():
                self.mode = ProductMode.VARIABLE_VARIATION_PARENT
            else:
                self.mode = ProductMode.SIMPLE_VARIATION_PARENT
            self.external_url = None
            ProductPackageLink.objects.filter(parent=self).delete()
        elif self.variation_parent:
            self.mode = ProductMode.VARIATION_CHILD
            ProductPackageLink.objects.filter(parent=self).delete()
            self.variation_children.clear()
            self.external_url = None
        else:
            self.mode = ProductMode.NORMAL

    def unlink_from_parent(self):
        if self.variation_parent:
            parent = self.variation_parent
            self.variation_parent = None
            self.save()
            parent.verify_mode()
            self.verify_mode()
            self.save()
            ProductVariationResult.objects.filter(result=self).delete()
            return True

    def link_to_parent(self, parent, variables=None, combination_hash=None):
        """
        :param parent: The parent to link to.
        :type parent: Product
        :param variables: Optional dict of {variable identifier: value identifier} for complex variable linkage
        :type variables: dict|None
        :param combination_hash: Optional combination hash (for variable variations), if precomputed. Mutually
                                 exclusive with `variables`
        :type combination_hash: str|None

        """
        if combination_hash:
            if variables:
                raise ValueError("`combination_hash` and `variables` are mutually exclusive")
            variables = True  # Simplifies the below invariant checks

        self._raise_if_cant_link_to_parent(parent, variables)

        self.unlink_from_parent()
        self.variation_parent = parent
        self.verify_mode()
        self.save()
        if not parent.is_variation_parent():
            parent.verify_mode()
            parent.save()

        if variables:
            if not combination_hash:  # No precalculated hash, need to figure that out
                combination_hash = get_combination_hash_from_variable_mapping(parent, variables=variables)

            pvr = ProductVariationResult.objects.create(
                product=parent,
                combination_hash=combination_hash,
                result=self
            )
            if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT:
                parent.verify_mode()
                parent.save()
            return pvr
        else:
            return True

    def _raise_if_cant_link_to_parent(self, parent, variables):
        """
        Validates relation possibility for `self.link_to_parent()`

        :param parent: parent product of self
        :type parent: Product
        :param variables:
        :type variables: dict|None
        """
        if parent.is_variation_child():
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (parent is a child already)"),
                code="multilevel"
            )
        if parent.mode == ProductMode.VARIABLE_VARIATION_PARENT and not variables:
            raise ImpossibleProductModeException(
                _("Parent is a variable variation parent, yet variables were not passed"),
                code="no_variables"
            )
        if parent.mode == ProductMode.SIMPLE_VARIATION_PARENT and variables:
            raise ImpossibleProductModeException(
                "Parent is a simple variation parent, yet variables were passed",
                code="extra_variables"
            )
        if self.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (this product is a simple variation parent)"),
                code="multilevel"
            )
        if self.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            raise ImpossibleProductModeException(
                _("Multilevel parentage hierarchies aren't supported (this product is a variable variation parent)"),
                code="multilevel"
            )

    def make_package(self, package_def):
        if self.mode != ProductMode.NORMAL:
            raise ImpossibleProductModeException(
                _("Product is currently not a normal product, can't turn into package"),
                code="abnormal"
            )

        for child_product, quantity in six.iteritems(package_def):
            if child_product.pk == self.pk:
                raise ImpossibleProductModeException(_("Package can't contain itself"), code="content")
            # :type child_product: Product
            if child_product.is_variation_parent():
                raise ImpossibleProductModeException(
                    _("Variation parents can not belong into a package"),
                    code="abnormal"
                )
            if child_product.is_container():
                raise ImpossibleProductModeException(_("Packages can't be nested"), code="multilevel")
            if quantity <= 0:
                raise ImpossibleProductModeException(_("Quantity %s is invalid") % quantity, code="quantity")
            ProductPackageLink.objects.create(parent=self, child=child_product, quantity=quantity)
        self.verify_mode()

    def get_package_child_to_quantity_map(self):
        if self.is_container():
            product_id_to_quantity = dict(
                ProductPackageLink.objects.filter(parent=self).values_list("child_id", "quantity")
            )
            products = dict((p.pk, p) for p in Product.objects.filter(pk__in=product_id_to_quantity.keys()))
            return {products[product_id]: quantity for (product_id, quantity) in six.iteritems(product_id_to_quantity)}
        return {}

    def is_variation_parent(self):
        return self.mode in (ProductMode.SIMPLE_VARIATION_PARENT, ProductMode.VARIABLE_VARIATION_PARENT)

    def is_variation_child(self):
        return (self.mode == ProductMode.VARIATION_CHILD)

    def get_variation_siblings(self):
        return Product.objects.filter(variation_parent=self.variation_parent).exclude(pk=self.pk)

    def is_package_parent(self):
        return (self.mode == ProductMode.PACKAGE_PARENT)

    def is_subscription_parent(self):
        return (self.mode == ProductMode.SUBSCRIPTION)

    def is_package_child(self):
        return ProductPackageLink.objects.filter(child=self).exists()

    def get_all_package_parents(self):
        return Product.objects.filter(pk__in=(
            ProductPackageLink.objects.filter(child=self).values_list("parent", flat=True)
        ))

    def get_all_package_children(self):
        return Product.objects.filter(pk__in=(
            ProductPackageLink.objects.filter(parent=self).values_list("child", flat=True)
        ))

    def get_public_media(self):
        return self.media.filter(enabled=True, public=True).exclude(kind=ProductMediaKind.IMAGE)

    def is_stocked(self):
        return (self.stock_behavior == StockBehavior.STOCKED)

    def is_container(self):
        return (self.is_package_parent() or self.is_subscription_parent())
Ejemplo n.º 28
0
class SuomiFiAccessLevel(TranslatableModel):
    translations = TranslatedFields(name=models.CharField(max_length=100),
                                    description=models.TextField(blank=True))
    shorthand = models.CharField(max_length=100, unique=True)
    attributes = models.ManyToManyField(SuomiFiUserAttribute)
Ejemplo n.º 29
0
class ShopProduct(MoneyPropped, TranslatableModel):
    shop = models.ForeignKey("Shop",
                             related_name="shop_products",
                             on_delete=models.CASCADE,
                             verbose_name=_("shop"))
    product = UnsavedForeignKey("Product",
                                related_name="shop_products",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    suppliers = models.ManyToManyField(
        "Supplier",
        related_name="shop_products",
        blank=True,
        verbose_name=_("suppliers"),
        help_text=
        _("List your suppliers here. Suppliers can be found in Product Settings - Suppliers."
          ))

    visibility = EnumIntegerField(
        ShopProductVisibility,
        default=ShopProductVisibility.ALWAYS_VISIBLE,
        db_index=True,
        verbose_name=_("visibility"),
        help_text=mark_safe_lazy(
            _("Select if you want your product to be seen and found by customers. "
              "<p>Not visible: Product will not be shown in your store front or found in search.</p>"
              "<p>Searchable: Product will be shown in search but not listed on any category page.</p>"
              "<p>Listed: Product will be shown on category pages but not shown in search results.</p>"
              "<p>Always Visible: Product will be shown in your store front and found in search.</p>"
              )))
    purchasable = models.BooleanField(default=True,
                                      db_index=True,
                                      verbose_name=_("purchasable"))
    visibility_limit = EnumIntegerField(
        ProductVisibility,
        db_index=True,
        default=ProductVisibility.VISIBLE_TO_ALL,
        verbose_name=_('visibility limitations'),
        help_text=
        _("Select whether you want your product to have special limitations on its visibility in your store. "
          "You can make products visible to all, visible to only logged in users, or visible only to certain "
          "customer groups."))
    visibility_groups = models.ManyToManyField(
        "ContactGroup",
        related_name='visible_products',
        verbose_name=_('visible for groups'),
        blank=True,
        help_text=
        _(u"Select the groups you would like to make your product visible for. "
          u"These groups are defined in Contacts Settings - Contact Groups."))
    backorder_maximum = QuantityField(
        default=0,
        blank=True,
        null=True,
        verbose_name=_('backorder maximum'),
        help_text=_(
            "The number of units that can be purchased after the product is out of stock. "
            "Set to blank for product to be purchasable without limits."))
    purchase_multiple = QuantityField(
        default=0,
        verbose_name=_('purchase multiple'),
        help_text=_(
            "Set this if the product needs to be purchased in multiples. "
            "For example, if the purchase multiple is set to 2, then customers are required to order the product "
            "in multiples of 2."))
    minimum_purchase_quantity = QuantityField(
        default=1,
        verbose_name=_('minimum purchase'),
        help_text=_(
            "Set a minimum number of products needed to be ordered for the purchase. "
            "This is useful for setting bulk orders and B2B purchases."))
    limit_shipping_methods = models.BooleanField(
        default=False,
        verbose_name=_("limited for shipping methods"),
        help_text=_(
            "Check this if you want to limit your product to use only select payment methods. "
            "You can select the payment method(s) in the field below."))
    limit_payment_methods = models.BooleanField(
        default=False,
        verbose_name=_("limited for payment methods"),
        help_text=_(
            "Check this if you want to limit your product to use only select payment methods. "
            "You can select the payment method(s) in the field below."))
    shipping_methods = models.ManyToManyField(
        "ShippingMethod",
        related_name='shipping_products',
        verbose_name=_('shipping methods'),
        blank=True,
        help_text=_(
            "Select the shipping methods you would like to limit the product to using. "
            "These are defined in Settings - Shipping Methods."))
    payment_methods = models.ManyToManyField(
        "PaymentMethod",
        related_name='payment_products',
        verbose_name=_('payment methods'),
        blank=True,
        help_text=_(
            "Select the payment methods you would like to limit the product to using. "
            "These are defined in Settings - Payment Methods."))
    primary_category = models.ForeignKey(
        "Category",
        related_name='primary_shop_products',
        verbose_name=_('primary category'),
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        help_text=_(
            "Choose the primary category for your product. "
            "This will be the main category for classification in the system. "
            "Your product can be found under this category in your store. "
            "Categories are defined in Products Settings - Categories."))
    categories = models.ManyToManyField(
        "Category",
        related_name='shop_products',
        verbose_name=_('categories'),
        blank=True,
        help_text=_(
            "Add secondary categories for your product. "
            "These are other categories that your product fits under and that it can be found by in your store."
        ))
    shop_primary_image = models.ForeignKey(
        "ProductMedia",
        null=True,
        blank=True,
        related_name="primary_image_for_shop_products",
        on_delete=models.SET_NULL,
        verbose_name=_("primary image"),
        help_text=
        _("Click this to set this image as the primary display image for your product."
          ))

    # the default price of this product in the shop
    default_price = PriceProperty('default_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    default_price_value = MoneyValueField(
        verbose_name=_("default price"),
        null=True,
        blank=True,
        help_text=_(
            "This is the default individual base unit (or multi-pack) price of the product. "
            "All discounts or coupons will be based off of this price."))

    minimum_price = PriceProperty('minimum_price_value', 'shop.currency',
                                  'shop.prices_include_tax')
    minimum_price_value = MoneyValueField(
        verbose_name=_("minimum price"),
        null=True,
        blank=True,
        help_text=
        _("This is the default price that the product cannot go under in your store, "
          "despite coupons or discounts being applied. "
          "This is useful to make sure your product price stays above cost."))

    display_unit = models.ForeignKey(
        DisplayUnit,
        null=True,
        blank=True,
        verbose_name=_("display unit"),
        help_text=_("Unit for displaying quantities of this product"))

    translations = TranslatedFields(
        name=models.CharField(
            blank=True,
            null=True,
            max_length=256,
            verbose_name=_('name'),
            help_text=
            _("Enter a descriptive name for your product. This will be its title in your store."
              )),
        description=models.TextField(
            blank=True,
            null=True,
            verbose_name=_(
                'description'),
            help_text=
            _("To make your product stand out, give it an awesome description. "
              "This is what will help your shoppers learn about your products. "
              "It will also help shoppers find them in the store and on the web."
              )),
        short_description=models.CharField(
            blank=True,
            null=True,
            max_length=150,
            verbose_name=_('short description'),
            help_text=
            _("Enter a short description for your product. "
              "The short description will be used to get the attention of your "
              "customer with a small but precise description of your product."
              )),
        status_text=models.CharField(
            max_length=128,
            blank=True,
            verbose_name=_('status text'),
            help_text=_(
                'This text will be shown alongside the product in the shop. '
                'It is useful for informing customers of special stock numbers or preorders. '
                '(Ex.: "Available in a month")')))

    class Meta:
        unique_together = ((
            "shop",
            "product",
        ), )

    def save(self, *args, **kwargs):
        self.clean()
        super(ShopProduct, self).save(*args, **kwargs)
        for supplier in self.suppliers.enabled():
            supplier.module.update_stock(product_id=self.product.id)

    def clean(self):
        pre_clean.send(type(self), instance=self)
        super(ShopProduct, self).clean()
        if self.display_unit:
            if self.display_unit.internal_unit != self.product.sales_unit:
                raise ValidationError({
                    'display_unit':
                    _("Invalid display unit: Internal unit of "
                      "the selected display unit does not match "
                      "with the sales unit of the product")
                })
        post_clean.send(type(self), instance=self)

    def is_list_visible(self):
        """
        Return True if this product should be visible in listings in general,
        without taking into account any other visibility limitations.
        :rtype: bool
        """
        if self.product.deleted:
            return False
        if not self.listed:
            return False
        if self.product.is_variation_child():
            return False
        return True

    @property
    def primary_image(self):
        if self.shop_primary_image_id:
            return self.shop_primary_image
        else:
            return self.product.primary_image

    @property
    def searchable(self):
        return self.visibility in (ShopProductVisibility.SEARCHABLE,
                                   ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def listed(self):
        return self.visibility in (ShopProductVisibility.LISTED,
                                   ShopProductVisibility.ALWAYS_VISIBLE)

    @property
    def visible(self):
        return not (self.visibility == ShopProductVisibility.NOT_VISIBLE)

    @property
    def public_primary_image(self):
        primary_image = self.primary_image
        return primary_image if primary_image and primary_image.public else None

    def get_visibility_errors(self, customer):
        if self.product.deleted:
            yield ValidationError(_('This product has been deleted.'),
                                  code="product_deleted")

        if customer and customer.is_all_seeing:  # None of the further conditions matter for omniscient customers.
            return

        if not self.visible:
            yield ValidationError(_('This product is not visible.'),
                                  code="product_not_visible")

        is_logged_in = (bool(customer) and not customer.is_anonymous)

        if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL:
            yield ValidationError(
                _('The Product is invisible to users not logged in.'),
                code="product_not_visible_to_anonymous")

        if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS:
            # TODO: Optimization
            user_groups = set(customer.groups.all().values_list("pk",
                                                                flat=True))
            my_groups = set(self.visibility_groups.values_list("pk",
                                                               flat=True))
            if not bool(user_groups & my_groups):
                yield ValidationError(
                    _('This product is not visible to your group.'),
                    code="product_not_visible_to_group")

        # TODO: Remove from Shuup 2.0
        for receiver, response in get_visibility_errors.send(
                ShopProduct, shop_product=self, customer=customer):
            warnings.warn("Visibility errors through signals are deprecated",
                          DeprecationWarning)
            for error in response:
                yield error

    def get_orderability_errors(self,
                                supplier,
                                quantity,
                                customer,
                                ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be orderable.

        Shop product to be orderable it needs to be visible visible and purchasable

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        for error in self.get_visibility_errors(customer):
            yield error

        for error in self.get_purchasability_errors(supplier, customer,
                                                    quantity, ignore_minimum):
            yield error

    def get_purchasability_errors(self,
                                  supplier,
                                  customer,
                                  quantity,
                                  ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be purchasable.

        Shop product to be purchasable it has to have purchasable attribute set on
        and pass all quantity and supplier checks.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shuup.core.models.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shuup.core.models.Contact
        :param ignore_minimum: Ignore any limitations caused by quantity minimums.
        :type ignore_minimum: bool
        :return: Iterable[ValidationError]
        """
        if not self.purchasable:
            yield ValidationError(_('The product is not purchasable'),
                                  code="not_purchasable")

        for error in self.get_quantity_errors(quantity, ignore_minimum):
            yield error

        for error in self.get_supplier_errors(supplier, customer, quantity,
                                              ignore_minimum):
            yield error

        # TODO: Remove from Shuup 2.0
        for receiver, response in get_orderability_errors.send(
                ShopProduct,
                shop_product=self,
                customer=customer,
                supplier=supplier,
                quantity=quantity):
            warnings.warn("Orderability errors through signals are deprecated",
                          DeprecationWarning)
            for error in response:
                yield error

    def get_quantity_errors(self, quantity, ignore_minimum):
        if not ignore_minimum and quantity < self.minimum_purchase_quantity:
            yield ValidationError(_(
                'The purchase quantity needs to be at least %d for this product.'
            ) % self.minimum_purchase_quantity,
                                  code="purchase_quantity_not_met")

        purchase_multiple = self.purchase_multiple
        if quantity > 0 and purchase_multiple > 0 and (quantity %
                                                       purchase_multiple) != 0:
            p = (quantity // purchase_multiple)
            smaller_p = max(purchase_multiple, p * purchase_multiple)
            larger_p = max(purchase_multiple, (p + 1) * purchase_multiple)
            render_qty = self.unit.render_quantity
            if larger_p == smaller_p:
                message = _("The product can only be ordered in multiples of "
                            "{package_size}, for example {amount}").format(
                                package_size=render_qty(purchase_multiple),
                                amount=render_qty(smaller_p))
            else:
                message = _("The product can only be ordered in multiples of "
                            "{package_size}, for example {smaller_amount} or "
                            "{larger_amount}").format(
                                package_size=render_qty(purchase_multiple),
                                smaller_amount=render_qty(smaller_p),
                                larger_amount=render_qty(larger_p))
            yield ValidationError(message, code="invalid_purchase_multiple")

    def get_supplier_errors(self, supplier, customer, quantity,
                            ignore_minimum):
        enabled_supplier_pks = self.suppliers.enabled().values_list("pk",
                                                                    flat=True)
        if supplier is None and not enabled_supplier_pks:
            # `ShopProduct` must have at least one `Supplier`.
            # If supplier is not given and the `ShopProduct` itself
            # doesn't have suppliers we cannot sell this product.
            yield ValidationError(_('The product has no supplier.'),
                                  code="no_supplier")

        if supplier and supplier.pk not in enabled_supplier_pks:
            yield ValidationError(_('The product is not supplied by %s.') %
                                  supplier,
                                  code="invalid_supplier")

        errors = []
        if self.product.mode == ProductMode.SIMPLE_VARIATION_PARENT:
            errors = self.get_orderability_errors_for_simple_variation_parent(
                supplier, customer)
        elif self.product.mode == ProductMode.VARIABLE_VARIATION_PARENT:
            errors = self.get_orderability_errors_for_variable_variation_parent(
                supplier, customer)
        elif self.product.is_package_parent():
            errors = self.get_orderability_errors_for_package_parent(
                supplier, customer, quantity, ignore_minimum)
        elif supplier:  # Test supplier orderability only for variation children and normal products
            errors = supplier.get_orderability_errors(self,
                                                      quantity,
                                                      customer=customer)

        for error in errors:
            yield error

    def get_orderability_errors_for_simple_variation_parent(
            self, supplier, customer):
        sellable = False
        for child_product in self.product.variation_children.all():
            try:
                child_shop_product = child_product.get_shop_instance(self.shop)
            except ShopProduct.DoesNotExist:
                continue

            if child_shop_product.is_orderable(
                    supplier=supplier,
                    customer=customer,
                    quantity=child_shop_product.minimum_purchase_quantity,
                    allow_cache=False):
                sellable = True
                break

        if not sellable:
            yield ValidationError(_("Product has no sellable children"),
                                  code="no_sellable_children")

    def get_orderability_errors_for_variable_variation_parent(
            self, supplier, customer):
        from shuup.core.models import ProductVariationResult
        sellable = False
        for combo in self.product.get_all_available_combinations():
            res = ProductVariationResult.resolve(self.product,
                                                 combo["variable_to_value"])
            if not res:
                continue
            try:
                child_shop_product = res.get_shop_instance(self.shop)
            except ShopProduct.DoesNotExist:
                continue

            if child_shop_product.is_orderable(
                    supplier=supplier,
                    customer=customer,
                    quantity=child_shop_product.minimum_purchase_quantity,
                    allow_cache=False):
                sellable = True
                break
        if not sellable:
            yield ValidationError(_("Product has no sellable children"),
                                  code="no_sellable_children")

    def get_orderability_errors_for_package_parent(self, supplier, customer,
                                                   quantity, ignore_minimum):
        for child_product, child_quantity in six.iteritems(
                self.product.get_package_child_to_quantity_map()):
            try:
                child_shop_product = child_product.get_shop_instance(
                    shop=self.shop, allow_cache=False)
            except ShopProduct.DoesNotExist:
                yield ValidationError("%s: Not available in %s" %
                                      (child_product, self.shop),
                                      code="invalid_shop")
            else:
                for error in child_shop_product.get_orderability_errors(
                        supplier=supplier,
                        quantity=(quantity * child_quantity),
                        customer=customer,
                        ignore_minimum=ignore_minimum):
                    message = getattr(error, "message", "")
                    code = getattr(error, "code", None)
                    yield ValidationError("%s: %s" % (child_product, message),
                                          code=code)

    def raise_if_not_orderable(self,
                               supplier,
                               customer,
                               quantity,
                               ignore_minimum=False):
        for message in self.get_orderability_errors(
                supplier=supplier,
                quantity=quantity,
                customer=customer,
                ignore_minimum=ignore_minimum):
            raise ProductNotOrderableProblem(message.args[0])

    def raise_if_not_visible(self, customer):
        for message in self.get_visibility_errors(customer=customer):
            raise ProductNotVisibleProblem(message.args[0])

    def is_orderable(self, supplier, customer, quantity, allow_cache=True):
        """
        Product to be orderable it needs to be visible and purchasable
        """
        key, val = context_cache.get_cached_value(
            identifier="is_orderable",
            item=self,
            context={"customer": customer},
            supplier=supplier,
            stock_managed=bool(supplier and supplier.stock_managed),
            quantity=quantity,
            allow_cache=allow_cache)
        if customer and val is not None:
            return val

        if not supplier:
            supplier = self.get_supplier(customer, quantity)

        for message in self.get_orderability_errors(supplier=supplier,
                                                    quantity=quantity,
                                                    customer=customer):
            if customer:
                context_cache.set_cached_value(key, False)
            return False

        if customer:
            context_cache.set_cached_value(key, True)
        return True

    def is_visible(self, customer):
        """
        Visible products is shown in store front based on customer
        or customer group limitations
        """
        for message in self.get_visibility_errors(customer=customer):
            return False
        return True

    def is_purchasable(self, supplier, customer, quantity):
        """
        Whether product can be purchasable
        """
        for message in self.get_purchasability_errors(supplier, customer,
                                                      quantity):
            return False
        return True

    @property
    def quantity_step(self):
        """
        Quantity step for purchasing this product.

        :rtype: decimal.Decimal

        Example:
            <input type="number" step="{{ shop_product.quantity_step }}">
        """
        step = self.purchase_multiple or self._sales_unit.quantity_step
        return self._sales_unit.round(step)

    @property
    def rounded_minimum_purchase_quantity(self):
        """
        The minimum purchase quantity, rounded to the sales unit's precision.

        :rtype: decimal.Decimal

        Example:
            <input type="number"
                min="{{ shop_product.rounded_minimum_purchase_quantity }}"
                value="{{ shop_product.rounded_minimum_purchase_quantity }}">

        """
        return self._sales_unit.round(self.minimum_purchase_quantity)

    @property
    def display_quantity_step(self):
        """
        Quantity step of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(self.unit.to_display(self.quantity_step),
                   self.unit.display_precision)

    @property
    def display_quantity_minimum(self):
        """
        Quantity minimum of this shop product in the display unit.

        Note: This can never be smaller than the display precision.
        """
        return max(self.unit.to_display(self.minimum_purchase_quantity),
                   self.unit.display_precision)

    @property
    def unit(self):
        """
        Unit of this product.

        :rtype: shuup.core.models.UnitInterface
        """
        return UnitInterface(self._sales_unit, self.display_unit)

    @property
    def _sales_unit(self):
        return self.product.sales_unit or PiecesSalesUnit()

    @property
    def images(self):
        return self.product.media.filter(
            shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering")

    @property
    def public_images(self):
        return self.images.filter(public=True)

    def get_supplier(self,
                     customer=None,
                     quantity=None,
                     shipping_address=None):
        supplier_strategy = cached_load(
            "SHUUP_SHOP_PRODUCT_SUPPLIERS_STRATEGY")
        kwargs = {
            "shop_product": self,
            "customer": customer,
            "quantity": quantity,
            "shipping_address": shipping_address
        }
        return supplier_strategy().get_supplier(**kwargs)

    def __str__(self):
        return self.get_name()

    def get_name(self):
        return self._safe_get_string("name")

    def get_description(self):
        return self._safe_get_string("description")

    def get_short_description(self):
        return self._safe_get_string("short_description")

    def _safe_get_string(self, key):
        return (self.safe_translation_getter(key, any_language=True) or
                self.product.safe_translation_getter(key, any_language=True))
Ejemplo n.º 30
0
class ProductMedia(TranslatableModel):
    identifier = InternalIdentifierField(unique=True)
    product = models.ForeignKey("Product", related_name="media", on_delete=models.CASCADE, verbose_name=_("product"))
    shops = models.ManyToManyField(
        "Shop",
        related_name="product_media",
        verbose_name=_("shops"),
        help_text=_("Select which shops you would like the product media to be visible in."),
    )
    kind = EnumIntegerField(
        ProductMediaKind,
        db_index=True,
        default=ProductMediaKind.GENERIC_FILE,
        verbose_name=_("kind"),
        help_text=_(
            "Select what type the media is. It can either be a normal file, part of the documentation, or a sample."
        ),
    )
    file = FilerFileField(blank=True, null=True, verbose_name=_("file"), on_delete=models.CASCADE)
    external_url = models.URLField(
        blank=True,
        null=True,
        verbose_name=_("URL"),
        help_text=_("Enter URL to external file. If this field is filled, the selected media doesn't apply."),
    )
    ordering = models.IntegerField(
        default=0,
        verbose_name=_("ordering"),
        help_text=_(
            "You can assign numerical values to images to tell the order in which they "
            "shall be displayed on the product page."
        ),
    )
    # Status
    enabled = models.BooleanField(db_index=True, default=True, verbose_name=_("enabled"))
    public = models.BooleanField(
        default=True,
        blank=True,
        verbose_name=_("public (shown on product page)"),
        help_text=_("Enable this if you want this image be shown on the product page. Enabled by default."),
    )
    purchased = models.BooleanField(
        default=False,
        blank=True,
        verbose_name=_("purchased (shown for finished purchases)"),
        help_text=_("Enable this if you want the product media to be shown for completed purchases."),
    )

    translations = TranslatedFields(
        title=models.CharField(
            blank=True,
            max_length=128,
            verbose_name=_("title"),
            help_text=_(
                "Choose a title for your product media. This will help it be found in your store and on the web."
            ),
        ),
        description=models.TextField(
            blank=True,
            verbose_name=_("description"),
            help_text=_(
                "Write a description for your product media. This will help it be found in your store and on the web."
            ),
        ),
    )

    class Meta:
        verbose_name = _("product attachment")
        verbose_name_plural = _("product attachments")
        ordering = [
            "ordering",
        ]

    def __str__(self):  # pragma: no cover
        return self.effective_title

    @property
    def effective_title(self):
        title = self.safe_translation_getter("title")
        if title:
            return title

        if self.file_id:
            return self.file.label

        if self.external_url:
            return self.external_url

        return _("attachment")

    @property
    def url(self):
        if self.external_url:
            return self.external_url
        if self.file:
            return self.file.url
        return ""

    @property
    def easy_thumbnails_thumbnailer(self):
        """
        Get `Thumbnailer` instance.

        Will return `None` if file cannot be thumbnailed.

        :rtype:easy_thumbnails.files.Thumbnailer|None
        """
        if not self.file_id:
            return None

        if self.kind != ProductMediaKind.IMAGE:
            return None

        return get_thumbnailer(self.file)

    def get_thumbnail(self, **kwargs):
        """
        Get thumbnail for image.

        This will return `None` if there is no file or kind is not `ProductMediaKind.IMAGE`

        :rtype: easy_thumbnails.files.ThumbnailFile|None
        """
        kwargs.setdefault("size", (64, 64))
        kwargs.setdefault("crop", True)  # sane defaults
        kwargs.setdefault("upscale", True)  # sane defaults

        if kwargs["size"] == (0, 0):
            return None

        thumbnailer = self.easy_thumbnails_thumbnailer

        if not thumbnailer:
            return None

        try:
            return thumbnailer.get_thumbnail(thumbnail_options=kwargs)
        except InvalidImageFormatError:
            return None