Exemplo n.º 1
0
class StockCount(models.Model):
    product = models.ForeignKey(
        "shoop.Product", related_name="+", editable=False, on_delete=models.CASCADE, verbose_name=_("product"))
    supplier = models.ForeignKey(
        "shoop.Supplier", editable=False, on_delete=models.CASCADE, verbose_name=_("supplier"))
    logical_count = QuantityField(default=0, editable=False, verbose_name=_("logical count"))
    physical_count = QuantityField(default=0, editable=False, verbose_name=_("physical count"))
    stock_value_value = MoneyValueField(default=0)
    stock_value = PriceProperty("stock_value_value", "currency", "includes_tax")
    stock_unit_price = PriceProperty("stock_unit_price_value", "currency", "includes_tax")

    class Meta:
        unique_together = [("product", "supplier")]

    @property
    def currency(self):
        return SHOOP_HOME_CURRENCY

    @property
    def includes_tax(self):
        return False

    @property
    def stock_unit_price_value(self):
        return (self.stock_value_value / self.logical_count if self.logical_count else 0)
Exemplo n.º 2
0
class BasketTotalAmountCondition(MoneyPropped, BasketCondition):
    identifier = "basket_amount_condition"
    name = _("Basket total value")

    amount = PriceProperty("amount_value", "campaign.shop.currency",
                           "campaign.shop.prices_include_tax")
    amount_value = MoneyValueField(default=None,
                                   blank=True,
                                   null=True,
                                   verbose_name=_("basket total amount"))

    def matches(self, basket, lines):
        return (basket.total_price_of_products.value >= self.amount_value)

    @property
    def description(self):
        return _(
            "Limit the campaign to match when it has at least the total value entered here worth of products."
        )

    @property
    def value(self):
        return self.amount_value

    @value.setter
    def value(self, value):
        self.amount_value = value
Exemplo n.º 3
0
class CgpPrice(MoneyPropped, models.Model):
    product = models.ForeignKey("shoop.Product",
                                related_name="+",
                                on_delete=models.CASCADE,
                                verbose_name=_("product"))
    shop = models.ForeignKey("shoop.Shop",
                             db_index=True,
                             on_delete=models.CASCADE,
                             verbose_name=_("shop"))
    group = models.ForeignKey("shoop.ContactGroup",
                              db_index=True,
                              on_delete=models.CASCADE,
                              verbose_name=_("contact group"))
    price = PriceProperty("price_value", "shop.currency",
                          "shop.prices_include_tax")
    price_value = MoneyValueField(verbose_name=_("price"))

    class Meta:
        unique_together = (('product', 'shop', 'group'), )
        verbose_name = _(u"product price")
        verbose_name_plural = _(u"product prices")

    def __repr__(self):
        return "<CgpPrice (p%s,s%s,g%s): price %s" % (
            self.product_id,
            self.shop_id,
            self.group_id,
            self.price,
        )
Exemplo n.º 4
0
    class Market(object):
        price = PriceProperty('value', 'currency', 'includes_tax')

        def __init__(self):
            self.value = 123
            self.currency = 'GBP'
            self.includes_tax = True
Exemplo n.º 5
0
class StockAdjustment(models.Model):
    product = models.ForeignKey("shoop.Product", related_name="+", on_delete=models.CASCADE, verbose_name=_("product"))
    supplier = models.ForeignKey("shoop.Supplier", on_delete=models.CASCADE, verbose_name=_("supplier"))
    created_on = models.DateTimeField(auto_now_add=True, editable=False, db_index=True, verbose_name=_("created on"))
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("created by"))
    delta = QuantityField(default=0, verbose_name=_("delta"))
    purchase_price_value = MoneyValueField(default=0)
    purchase_price = PriceProperty("purchase_price_value", "currency", "includes_tax")

    @property
    def currency(self):
        return SHOOP_HOME_CURRENCY

    @property
    def includes_tax(self):
        return False
Exemplo n.º 6
0
class ShopProduct(MoneyPropped, models.Model):
    shop = models.ForeignKey("Shop",
                             related_name="shop_products",
                             on_delete=models.CASCADE)
    product = UnsavedForeignKey("Product",
                                related_name="shop_products",
                                on_delete=models.CASCADE)
    suppliers = models.ManyToManyField("Supplier",
                                       related_name="shop_products",
                                       blank=True)

    visible = models.BooleanField(default=True, db_index=True)
    listed = models.BooleanField(default=True, db_index=True)
    purchasable = models.BooleanField(default=True, db_index=True)
    searchable = models.BooleanField(default=True, db_index=True)
    visibility_limit = EnumIntegerField(
        ProductVisibility,
        db_index=True,
        default=ProductVisibility.VISIBLE_TO_ALL,
        verbose_name=_('visibility limitations'))
    visibility_groups = models.ManyToManyField(
        "ContactGroup",
        related_name='visible_products',
        verbose_name=_('visible for groups'),
        blank=True)
    purchase_multiple = QuantityField(default=0,
                                      verbose_name=_('purchase multiple'))
    minimum_purchase_quantity = QuantityField(
        default=1, verbose_name=_('minimum purchase'))
    limit_shipping_methods = models.BooleanField(default=False)
    limit_payment_methods = models.BooleanField(default=False)
    shipping_methods = models.ManyToManyField(
        "ShippingMethod",
        related_name='shipping_products',
        verbose_name=_('shipping methods'),
        blank=True)
    payment_methods = models.ManyToManyField("PaymentMethod",
                                             related_name='payment_products',
                                             verbose_name=_('payment methods'),
                                             blank=True)
    primary_category = models.ForeignKey("Category",
                                         related_name='primary_shop_products',
                                         verbose_name=_('primary category'),
                                         blank=True,
                                         null=True,
                                         on_delete=models.PROTECT)
    categories = models.ManyToManyField("Category",
                                        related_name='shop_products',
                                        verbose_name=_('categories'),
                                        blank=True)
    shop_primary_image = models.ForeignKey(
        "ProductMedia",
        null=True,
        blank=True,
        related_name="primary_image_for_shop_products",
        on_delete=models.SET_NULL)

    # 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)

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

    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.visible:
            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

    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")

        for receiver, response in get_visibility_errors.send(
                ShopProduct, shop_product=self, customer=customer):
            for error in response:
                yield error

    # TODO: Refactor get_orderability_errors, it's too complex
    def get_orderability_errors(  # noqa (C901)
            self,
            supplier,
            quantity,
            customer,
            ignore_minimum=False):
        """
        Yield ValidationErrors that would cause this product to not be orderable.

        :param supplier: Supplier to order this product from. May be None.
        :type supplier: shoop.core.models.suppliers.Supplier
        :param quantity: Quantity to order.
        :type quantity: int|Decimal
        :param customer: Customer contact.
        :type customer: shoop.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

        if supplier is None and not self.suppliers.exists():
            # `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 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")

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

        if self.product.is_package_parent():
            for child_product, child_quantity in six.iteritems(
                    self.product.get_package_child_to_quantity_map()):
                child_shop_product = child_product.get_shop_instance(
                    shop=self.shop)
                if not child_shop_product:
                    yield ValidationError("%s: Not available in %s" %
                                          (child_product, self.shop),
                                          code="invalid_shop")
                for error in child_shop_product.get_orderability_errors(
                        supplier=supplier,
                        quantity=(quantity * child_quantity),
                        customer=customer,
                        ignore_minimum=ignore_minimum):
                    code = getattr(error, "code", None)
                    yield ValidationError("%s: %s" % (child_product, error),
                                          code=code)

        if supplier and self.product.stock_behavior == StockBehavior.STOCKED:
            for error in supplier.get_orderability_errors(self,
                                                          quantity,
                                                          customer=customer):
                yield error

        purchase_multiple = self.purchase_multiple
        if quantity > 0 and purchase_multiple > 1 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)
            if larger_p == smaller_p:
                message = _(
                    'The product can only be ordered in multiples of %(package_size)s, '
                    'for example %(smaller_p)s %(unit)s.') % {
                        "package_size": purchase_multiple,
                        "smaller_p": smaller_p,
                        "unit": self.product.sales_unit,
                    }
            else:
                message = _(
                    'The product can only be ordered in multiples of %(package_size)s, '
                    'for example %(smaller_p)s or %(larger_p)s %(unit)s.') % {
                        "package_size": purchase_multiple,
                        "smaller_p": smaller_p,
                        "larger_p": larger_p,
                        "unit": self.product.sales_unit,
                    }
            yield ValidationError(message, code="invalid_purchase_multiple")

        for receiver, response in get_orderability_errors.send(
                ShopProduct,
                shop_product=self,
                customer=customer,
                supplier=supplier,
                quantity=quantity):
            for error in response:
                yield error

    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):
        for message in self.get_orderability_errors(supplier=supplier,
                                                    quantity=quantity,
                                                    customer=customer):
            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 }}">
        """
        if self.purchase_multiple:
            return self.purchase_multiple
        return self.product.sales_unit.quantity_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.product.sales_unit.round(self.minimum_purchase_quantity)

    @property
    def images(self):
        return self.product.media.filter(
            shops=self.shop, kind=ProductMediaKind.IMAGE).order_by("ordering")
Exemplo n.º 7
0
class OrderLine(MoneyPropped, models.Model, Priceful):
    order = UnsavedForeignKey("Order",
                              related_name='lines',
                              on_delete=models.PROTECT,
                              verbose_name=_('order'))
    product = UnsavedForeignKey("Product",
                                blank=True,
                                null=True,
                                related_name="order_lines",
                                on_delete=models.PROTECT,
                                verbose_name=_('product'))
    supplier = UnsavedForeignKey("Supplier",
                                 blank=True,
                                 null=True,
                                 related_name="order_lines",
                                 on_delete=models.PROTECT,
                                 verbose_name=_('supplier'))

    parent_line = UnsavedForeignKey("self",
                                    related_name="child_lines",
                                    blank=True,
                                    null=True,
                                    on_delete=models.PROTECT,
                                    verbose_name=_('parent line'))
    ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
    type = EnumIntegerField(OrderLineType,
                            default=OrderLineType.PRODUCT,
                            verbose_name=_('line type'))
    sku = models.CharField(max_length=48,
                           blank=True,
                           verbose_name=_('line SKU'))
    text = models.CharField(max_length=256, verbose_name=_('line text'))
    accounting_identifier = models.CharField(
        max_length=32, blank=True, verbose_name=_('accounting identifier'))
    require_verification = models.BooleanField(
        default=False, verbose_name=_('require verification'))
    verified = models.BooleanField(default=False, verbose_name=_('verified'))
    extra_data = JSONField(blank=True, null=True, verbose_name=_('extra data'))

    # The following fields govern calculation of the prices
    quantity = QuantityField(verbose_name=_('quantity'), default=1)
    base_unit_price = PriceProperty('base_unit_price_value', 'order.currency',
                                    'order.prices_include_tax')
    discount_amount = PriceProperty('discount_amount_value', 'order.currency',
                                    'order.prices_include_tax')

    base_unit_price_value = MoneyValueField(
        verbose_name=_('unit price amount (undiscounted)'), default=0)
    discount_amount_value = MoneyValueField(
        verbose_name=_('total amount of discount'), default=0)

    objects = OrderLineManager()

    class Meta:
        verbose_name = _('order line')
        verbose_name_plural = _('order lines')

    def __str__(self):
        return "%dx %s (%s)" % (self.quantity, self.text,
                                self.get_type_display())

    @property
    def tax_amount(self):
        """
        :rtype: shoop.utils.money.Money
        """
        zero = Money(0, self.order.currency)
        return sum((x.amount for x in self.taxes.all()), zero)

    def save(self, *args, **kwargs):
        if not self.sku:
            self.sku = u""
        if self.type == OrderLineType.PRODUCT and not self.product_id:
            raise ValidationError(
                "Product-type order line can not be saved without a set product"
            )

        if self.product_id and self.type != OrderLineType.PRODUCT:
            raise ValidationError(
                "Order line has product but is not of Product type")

        if self.product_id and not self.supplier_id:
            raise ValidationError("Order line has product but no supplier")

        super(OrderLine, self).save(*args, **kwargs)
        if self.product_id:
            self.supplier.module.update_stock(self.product_id)
Exemplo n.º 8
0
class Campaign(MoneyPropped, TranslatableModel):
    admin_url_suffix = None

    shop = models.ForeignKey(Shop, verbose_name=_("shop"), help_text=_("The shop where campaign is active."))
    name = models.CharField(max_length=120, verbose_name=_("name"), help_text=_("The name for this campaign."))

    # translations in subclass

    identifier = InternalIdentifierField(unique=True)
    discount_percentage = models.DecimalField(
        max_digits=6, decimal_places=5, blank=True, null=True,
        verbose_name=_("discount percentage"),
        help_text=_("The discount percentage for this campaign."))
    discount_amount = PriceProperty("discount_amount_value", "shop.currency", "shop.prices_include_tax")
    discount_amount_value = MoneyValueField(
        default=None, blank=True, null=True,
        verbose_name=_("discount amount value"),
        help_text=_("Flat amount of discount. Mutually exclusive with percentage."))
    active = models.BooleanField(default=False, verbose_name=_("active"))
    start_datetime = models.DateTimeField(null=True, blank=True, verbose_name=_("start date and time"))
    end_datetime = models.DateTimeField(null=True, blank=True, verbose_name=_("end date and time"))
    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"))

    # objects = CampaignManager()

    class Meta:
        abstract = True
        verbose_name = _('Campaign')
        verbose_name_plural = _('Campaigns')

    def is_available(self):
        if not self.active:  # move to manager?
            return False
        if self.start_datetime and self.end_datetime:
            if self.start_datetime <= now() <= self.end_datetime:
                return True
            return False
        elif self.start_datetime and not self.end_datetime:
            if self.start_datetime > now():
                return False
        elif not self.start_datetime and self.end_datetime:
            if self.end_datetime < now():
                return False
        return True

    def save(self, **kwargs):
        if self.discount_percentage and self.discount_amount_value:
            raise ValidationError(_("You should only define either discount percentage or amount."))

        if not (self.discount_percentage or self.discount_amount_value):
            raise ValidationError(_("You must define discount percentage or amount."))

        super(Campaign, self).save(**kwargs)
Exemplo n.º 9
0
class FooItem(MoneyPropped, Base):
    price = PriceProperty('value', 'foo.currency', 'foo.includes_tax')