예제 #1
0
class Licence(TranslatableModel):
    """
    Licence model.

    Instances of this models should only be created by administrators.
    """

    name = TranslatedField()
    content = TranslatedField()
    logo = FilerImageField(verbose_name=_("logo"),
                           on_delete=models.PROTECT,
                           related_name="licence")
    url = models.CharField(_("url"), blank=True, max_length=255)
    # Deprecated non-translated fields for name & content
    # Kept around to avoid a breaking change wrt. blue-green deployments
    name_deprecated = models.CharField(_("name"),
                                       db_column="name",
                                       max_length=200)
    content_deprecated = models.TextField(_("content"),
                                          blank=False,
                                          db_column="content",
                                          default="")

    class Meta:
        db_table = "richie_licence"
        verbose_name = _("licence")

    def __str__(self):
        """Human representation of a licence."""
        return "{model}: {name}".format(model=self._meta.verbose_name.title(),
                                        name=self.name)
예제 #2
0
파일: _units.py 프로젝트: rounak06/shuup
class SalesUnit(_ShortNameToSymbol, TranslatableShuupModel):
    identifier = InternalIdentifierField(unique=True)
    decimals = models.PositiveSmallIntegerField(
        default=0,
        verbose_name=_(u"allowed decimal places"),
        help_text=_(
            "The number of decimal places allowed by this sales unit."
            "Set this to a value greater than zero if products with this sales unit can be sold in fractional quantities."
        ))

    name = TranslatedField()
    symbol = TranslatedField()

    class Meta:
        verbose_name = _('sales unit')
        verbose_name_plural = _('sales units')

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

    @property
    def allow_fractions(self):
        return self.decimals > 0

    @cached_property
    def quantity_step(self):
        """
        Get the quantity increment for the amount of decimals this unit allows.

        For zero decimals, this will be 1; for one decimal, 0.1; etc.

        :return: Decimal in (0..1].
        :rtype: Decimal
        """

        # This particular syntax (`10 ^ -n`) is the same that `bankers_round` uses
        # to figure out the quantizer.

        return Decimal(10)**(-int(self.decimals))

    def round(self, value):
        return bankers_round(parse_decimal_string(value), self.decimals)

    @property
    def display_unit(self):
        """
        Default display unit of this sales unit.

        Get a `DisplayUnit` object, which has this sales unit as its
        internal unit and is marked as a default, or if there is no
        default display unit for this sales unit, then a proxy object.
        The proxy object has the same display unit interface and mirrors
        the properties of the sales unit, such as symbol and decimals.

        :rtype: DisplayUnit
        """
        return (get_display_unit(self)
                if self.pk else None) or SalesUnitAsDisplayUnit(self)
예제 #3
0
class Post(TranslatableModel):
    slug = models.SlugField(unique=True, max_length=250)
    title = TranslatedField()
    body = TranslatedField()

    # translations = TranslatedFields(
    #     title=models.CharField(max_length=200, unique=True, ),
    #     body = models.TextField(max_length=5000)
    # )

    def __str__(self):
        return self.slug
예제 #4
0
class Traduction(TranslatableModel):
    title = TranslatedField(any_language=True,)
    slug = TranslatedField()
    
    date_add = models.DateTimeField(auto_now_add=True, null=True)
    date_update = models.DateTimeField(auto_now=True, null=True)
    status = models.BooleanField(default=True, null=True)
    
    class Meta():
        verbose_name = 'Traduction'
        verbose_name_plural = 'Traductions'
        
    def __unicode__(self):
        return self.title
예제 #5
0
class WaivingCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Waiving cost")
    help_text = _("Add cost to price of the service if total price "
                  "of products is less than a waive limit.")

    price_value = MoneyValueField(help_text=_(
        "The cost to apply to this service if the total price is below the waive limit."
    ))
    waive_limit_value = MoneyValueField(help_text=_(
        "The total price of products at which this service cost is waived."))
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("description"),
        help_text=_(
            "The order line text to display when this behavior is applied.")),
                                    )

    def get_costs(self, service, source):
        waive_limit = source.create_price(self.waive_limit_value)
        product_total = source.total_price_of_products
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter('description')
        zero_price = source.create_price(0)
        if product_total and product_total >= waive_limit:
            yield ServiceCost(zero_price, description, base_price=price)
        else:
            yield ServiceCost(price, description)
예제 #6
0
class WeightBasedPriceRange(TranslatableModel):
    component = models.ForeignKey("WeightBasedPricingBehaviorComponent",
                                  related_name="ranges",
                                  on_delete=models.CASCADE)
    min_value = MeasurementField(
        unit="g",
        verbose_name=_("min weight (g)"),
        blank=True,
        null=True,
        help_text=_("The minimum weight, in grams, for this price to apply."))
    max_value = MeasurementField(
        unit="g",
        verbose_name=_("max weight (g)"),
        blank=True,
        null=True,
        help_text=_(
            "The maximum weight, in grams, before this price no longer applies."
        ))
    price_value = MoneyValueField(help_text=_(
        "The cost to apply to this service when the weight criteria is met."))
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("description"),
        help_text=_(
            "The order line text to display when this behavior is applied.")),
                                    )

    def matches_to_value(self, value):
        return _is_in_range(value, self.min_value, self.max_value)
예제 #7
0
class Picture(TranslatableModel):
    """Picture database model."""

    image_nr = models.IntegerField(help_text="Just a dummy number")
    caption = TranslatedField()

    class Meta:
        verbose_name = _("picture")
        verbose_name_plural = _("pictures")

    def __str__(self):
        return self.caption
class ExperimentChallengeTimelineEntry(TimeStampedModel, TranslatableModel):
    date = models.DateField(verbose_name=_('date'), )
    content = TranslatedField()
    experiment_challenge = models.ForeignKey(
        on_delete=models.CASCADE,
        to='experiments.ExperimentChallenge',
        verbose_name=_('experiment challenge'),
    )

    class Meta:
        ordering = ('date', 'created_at')
        verbose_name = _('experiment challenge timeline entry')
        verbose_name_plural = _('experiment challenge timeline entries')
예제 #9
0
class FixedCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Fixed cost")
    help_text = _("Add fixed cost to price of the service.")

    price_value = MoneyValueField()
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100, blank=True, verbose_name=_("description")), )

    def get_costs(self, service, source):
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter('description')
        yield ServiceCost(price, description)
예제 #10
0
class WeightBasedPriceRange(TranslatableModel):
    component = models.ForeignKey("WeightBasedPricingBehaviorComponent",
                                  related_name="ranges",
                                  on_delete=models.CASCADE)
    min_value = MeasurementField(unit="g",
                                 verbose_name=_("min weight"),
                                 blank=True,
                                 null=True)
    max_value = MeasurementField(unit="g",
                                 verbose_name=_("max weight"),
                                 blank=True,
                                 null=True)
    price_value = MoneyValueField()
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100, blank=True, verbose_name=_("description")), )

    def matches_to_value(self, value):
        return _is_in_range(value, self.min_value, self.max_value)
예제 #11
0
class FixedCostBehaviorComponent(TranslatableServiceBehaviorComponent):
    name = _("Fixed cost")
    help_text = _("Add a fixed cost to the price of the service.")

    price_value = MoneyValueField(
        help_text=_("The fixed cost to apply to this service."))
    description = TranslatedField(any_language=True)

    translations = TranslatedFields(description=models.CharField(
        max_length=100,
        blank=True,
        verbose_name=_("description"),
        help_text=_(
            "The order line text to display when this behavior is applied."),
    ), )

    def get_costs(self, service, source):
        price = source.create_price(self.price_value)
        description = self.safe_translation_getter("description")
        yield ServiceCost(price, description)
예제 #12
0
class ServiceProvider(PolymorphicTranslatableShuupModel):
    """
    Entity that provides services.

    Good examples of service providers are `Carrier` and
    `PaymentProcessor`.

    When subclassing `ServiceProvider`, set value for `service_model`
    class attribute. It should be a model class, which is a subclass of
    `Service`.
    """

    identifier = InternalIdentifierField(unique=True)
    enabled = models.BooleanField(
        default=True,
        verbose_name=_("enabled"),
        help_text=
        _("Enable this if this service provider can be used when placing orders."
          ),
    )
    name = TranslatedField(any_language=True)
    logo = FilerImageField(blank=True,
                           null=True,
                           on_delete=models.SET_NULL,
                           verbose_name=_("logo"))

    base_translations = TranslatedFields(name=models.CharField(
        max_length=100,
        verbose_name=_("name"),
        help_text=_("The service provider name.")), )

    shops = models.ManyToManyField(
        "shuup.Shop",
        verbose_name=_("shops"),
        related_name="service_providers",
        help_text=
        _("This service provider will be available only for order sources of the given shop. "
          "If blank, this service provider is available for any order source."
          ),
        blank=True,
    )
    supplier = models.ForeignKey(
        "shuup.Supplier",
        on_delete=models.CASCADE,
        verbose_name=_("supplier"),
        related_name="service_providers",
        help_text=
        _("This service provider will be available only for order sources that contain "
          "all items from the configured supplier. If blank, this service provider is "
          "available for any order source."),
        blank=True,
        null=True,
    )

    #: Model class of the provided services (subclass of `Service`)
    service_model = None

    def get_service_choices(self):
        """
        Get all service choices of this provider.

        Subclasses should implement this method.

        :rtype: list[ServiceChoice]
        """
        raise NotImplementedError

    def create_service(self, choice_identifier, **kwargs):
        """
        Create a service for a given choice identifier.

        Subclass implementation may attach some `behavior components
        <ServiceBehaviorComponent>` to the created service.

        Subclasses should provide implementation for `_create_service`
        or override it. Base class implementation calls the
        `_create_service` method with resolved `choice_identifier`.

        :type choice_identifier: str|None
        :param choice_identifier:
          Identifier of the service choice to use.  If None, use the
          default service choice.
        :rtype: shuup.core.models.Service
        """
        if choice_identifier is None:
            choice_identifier = self.get_service_choices()[0].identifier
        return self._create_service(choice_identifier, **kwargs)

    def _create_service(self, choice_identifier, **kwargs):
        """
        Create a service for a given choice identifier.

        :type choice_identifier: str
        :rtype: shuup.core.models.Service
        """
        raise NotImplementedError

    def get_effective_name(self, service, source):
        """
        Get effective name of the service for a given order source.

        Base class implementation will just return name of the given
        service, but that may be changed in a subclass.

        :type service: shuup.core.models.Service
        :type source: shuup.core.order_creator.OrderSource
        :rtype: str
        """
        return service.name
예제 #13
0
class Service(TranslatableShuupModel):
    """
    Abstract base model for services.

    Each enabled service should be linked to a service provider and
    should have a choice identifier specified in its `choice_identifier`
    field. The choice identifier should be valid for the service
    provider, i.e. it should be one of the `ServiceChoice.identifier`
    values returned by the `ServiceProvider.get_service_choices` method.
    """

    identifier = InternalIdentifierField(unique=True,
                                         verbose_name=_("identifier"))
    enabled = models.BooleanField(
        default=False,
        verbose_name=_("enabled"),
        help_text=_(
            "Enable this if this service should be selectable on checkout."),
    )
    shop = models.ForeignKey(on_delete=models.CASCADE,
                             to=Shop,
                             verbose_name=_("shop"),
                             help_text=_("The shop for this service."))
    supplier = models.ForeignKey(
        "shuup.Supplier",
        verbose_name=_("supplier"),
        on_delete=models.CASCADE,
        help_text=_(
            "The supplier for this service. This service will be available only for order sources "
            "that contain all items from this supplier."),
        null=True,
        blank=True,
    )
    choice_identifier = models.CharField(blank=True,
                                         max_length=64,
                                         verbose_name=_("choice identifier"))

    # These are for migrating old methods to new architecture
    old_module_identifier = models.CharField(max_length=64, blank=True)
    old_module_data = JSONField(blank=True, null=True)

    name = TranslatedField(any_language=True)
    description = TranslatedField()
    logo = FilerImageField(blank=True,
                           null=True,
                           on_delete=models.SET_NULL,
                           verbose_name=_("logo"))
    tax_class = models.ForeignKey(
        "TaxClass",
        on_delete=models.PROTECT,
        verbose_name=_("tax class"),
        help_text=
        _("The tax class to use for this service. Define by searching for `Tax Classes`."
          ),
    )

    behavior_components = models.ManyToManyField(
        "ServiceBehaviorComponent", verbose_name=_("behavior components"))
    labels = models.ManyToManyField("Label",
                                    blank=True,
                                    verbose_name=_("labels"))

    objects = ServiceQuerySet.as_manager()

    class Meta:
        abstract = True

    @property
    def provider(self):
        """
        :rtype: shuup.core.models.ServiceProvider
        """
        return getattr(self, self.provider_attr)

    def get_effective_name(self, source):
        """
        Get an effective name of the service for a given order source.

        By default, effective name is the same as name of this service,
        but if there is a service provider with a custom implementation
        for `~shuup.core.models.ServiceProvider.get_effective_name`
        method, then this can be different.

        :type source: shuup.core.order_creator.OrderSource
        :rtype: str
        """
        if not self.provider:
            return self.name
        return self.provider.get_effective_name(self, source)

    def is_available_for(self, source):
        """
        Return true if service is available for a given source.

        :type source: shuup.core.order_creator.OrderSource
        :rtype: bool
        """
        return not any(self.get_unavailability_reasons(source))

    def get_unavailability_reasons(self, source):
        """
        Get reasons of being unavailable for a given source.

        :type source: shuup.core.order_creator.OrderSource
        :rtype: Iterable[ValidationError]
        """
        if not self.provider or not self.provider.enabled or not self.enabled:
            yield ValidationError(_("%s is disabled.") % self, code="disabled")

        if source.shop.id != self.shop_id:
            yield ValidationError(_("%s is for different shop.") % self,
                                  code="wrong_shop")

        for component in self.behavior_components.all():
            for reason in component.get_unavailability_reasons(self, source):
                yield reason

    def get_total_cost(self, source):
        """
        Get total cost of this service for items in a given source.

        :type source: shuup.core.order_creator.OrderSource
        :rtype: PriceInfo
        """
        return _sum_costs(self.get_costs(source), source)

    def get_costs(self, source):
        """
        Get costs of this service for items in a given source.

        :type source: shuup.core.order_creator.OrderSource
        :return: description, price and tax class of the costs.
        :rtype: Iterable[ServiceCost]
        """
        for component in self.behavior_components.all():
            for cost in component.get_costs(self, source):
                yield cost

    def get_lines(self, source):
        """
        Get lines for a given source.

        Lines are created based on costs. Costs without descriptions are
        combined to a single line.

        :type source: shuup.core.order_creator.OrderSource
        :rtype: Iterable[shuup.core.order_creator.SourceLine]
        """
        for (num, line_data) in enumerate(self._get_line_data(source), 1):
            (price_info, tax_class, text) = line_data
            yield self._create_line(source, num, price_info, tax_class, text)

    def _get_line_data(self, source):
        # Split to costs with and without description
        costs_with_description = []
        costs_without_description = []
        for cost in self.get_costs(source):
            if cost.description:
                costs_with_description.append(cost)
            else:
                assert cost.tax_class is None
                costs_without_description.append(cost)

        if not (costs_with_description or costs_without_description):
            costs_without_description = [ServiceCost(source.create_price(0))]

        effective_name = self.get_effective_name(source)

        # Yield the combined cost first
        if costs_without_description:
            combined_price_info = _sum_costs(costs_without_description, source)
            yield (combined_price_info, self.tax_class, effective_name)

        # Then the costs with description, one line for each cost
        for cost in costs_with_description:
            tax_class = cost.tax_class or self.tax_class
            text = _("%(service_name)s: %(sub_item)s") % {
                "service_name": effective_name,
                "sub_item": cost.description,
            }
            yield (cost.price_info, tax_class, text)

    def _create_line(self, source, num, price_info, tax_class, text):
        return source.create_line(
            line_id=self._generate_line_id(num),
            type=self.line_type,
            quantity=price_info.quantity,
            text=text,
            base_unit_price=price_info.base_unit_price,
            discount_amount=price_info.discount_amount,
            tax_class=tax_class,
            supplier=self.supplier,
            shop=self.shop,
        )

    def _generate_line_id(self, num):
        return "%s-%02d-%s" % (self.line_type.name.lower(), num, uuid4().hex)

    def _make_sure_is_usable(self):
        if not self.provider:
            raise ValueError("Error! %r has no %s." %
                             (self, self.provider_attr))
        if not self.enabled:
            raise ValueError("Error! %r is disabled." % (self, ))
        if not self.provider.enabled:
            raise ValueError("Error! %s of %r is disabled." %
                             (self.provider_attr, self))
예제 #14
0
class ServiceProvider(PolymorphicTranslatableShoopModel):
    """
    Entity that provides services.

    Good examples of service providers are `Carrier` and
    `PaymentProcessor`.

    When subclassing `ServiceProvider`, set value for `service_model`
    class attribute.  It should be a model class which is subclass of
    `Service`.
    """
    identifier = InternalIdentifierField(unique=True)
    enabled = models.BooleanField(default=True, verbose_name=_("enabled"))
    name = TranslatedField(any_language=True)
    logo = FilerImageField(blank=True,
                           null=True,
                           on_delete=models.SET_NULL,
                           verbose_name=_("logo"))

    base_translations = TranslatedFields(name=models.CharField(
        max_length=100, verbose_name=_("name")), )

    #: Model class of the provided services (subclass of `Service`)
    service_model = None

    def get_service_choices(self):
        """
        Get all service choices of this provider.

        Subclasses should implement this method.

        :rtype: list[ServiceChoice]
        """
        raise NotImplementedError

    def create_service(self, choice_identifier, **kwargs):
        """
        Create a service for given choice identifier.

        Subclass implementation may attach some `behavior components
        <ServiceBehaviorComponent>` to the created service.

        Subclasses should provide implementation for `_create_service`
        or override this.  Base class implementation calls the
        `_create_service` method with resolved `choice_identifier`.

        :type choice_identifier: str|None
        :param choice_identifier:
          Identifier of the service choice to use.  If None, use the
          default service choice.
        :rtype: shoop.core.models.Service
        """
        if choice_identifier is None:
            choice_identifier = self.get_service_choices()[0].identifier
        return self._create_service(choice_identifier, **kwargs)

    def _create_service(self, choice_identifier, **kwargs):
        """
        Create a service for given choice identifier.

        :type choice_identifier: str
        :rtype: shoop.core.models.Service
        """
        raise NotImplementedError

    def get_effective_name(self, service, source):
        """
        Get effective name of the service for given order source.

        Base class implementation will just return name of the given
        service, but that may be changed in a subclass.

        :type service: shoop.core.models.Service
        :type source: shoop.core.order_creator.OrderSource
        :rtype: str
        """
        return service.name
예제 #15
0
파일: course.py 프로젝트: EDUlib/richie
class CourseRun(TranslatableModel):
    """
    The course run represents and records the occurence of a course between a start
    and an end date.
    """

    direct_course = models.ForeignKey(Course,
                                      on_delete=models.CASCADE,
                                      related_name="runs")
    # We register the foreign key in "draft_course_run" and not in "public_course_run"
    # so that the public course run gets deleted by cascade in the database when the
    # draft page is deleted. Doing it the other way would be fragile.
    draft_course_run = models.OneToOneField(
        "self",
        on_delete=models.CASCADE,
        null=True,
        editable=False,
        related_name="public_course_run",
    )
    sync_mode = models.CharField(
        max_length=20,
        choices=CourseRunSyncMode.choices,
        default=CourseRunSyncMode.MANUAL,
    )

    title = TranslatedField()
    resource_link = models.CharField(_("resource link"),
                                     max_length=200,
                                     blank=True,
                                     null=True)
    start = models.DateTimeField(_("course start"), blank=True, null=True)
    end = models.DateTimeField(_("course end"), blank=True, null=True)
    enrollment_start = models.DateTimeField(_("enrollment start"),
                                            blank=True,
                                            null=True)
    enrollment_end = models.DateTimeField(_("enrollment end"),
                                          blank=True,
                                          null=True)
    languages = MultiSelectField(
        max_choices=50,
        max_length=255,  # MySQL does not allow max_length > 255
        # Language choices are made lazy so that we can override them in our tests.
        # When set directly, they are evaluated too early and can't be changed with the
        # "override_settings" utility.
        choices=lazy(lambda: ALL_LANGUAGES, tuple)(),
        help_text=_(
            "The list of languages in which the course content is available."),
    )
    enrollment_count = models.PositiveIntegerField(
        _("enrollment count"),
        default=0,
        blank=True,
        help_text=_("The number of enrolled students"),
    )

    class Meta:
        db_table = "richie_course_run"
        verbose_name = _("course run")
        verbose_name_plural = _("course runs")

    def __str__(self):
        """Human representation of a course run."""
        start = f"{self.start:%y/%m/%d %H:%M} - " if self.start else ""
        return f"Course run {self.id!s} starting {start:s}"

    def copy_translations(self, oldinstance, language=None):
        """Copy translation objects for a language if provided or for all languages."""
        query = CourseRunTranslation.objects.filter(master=oldinstance)
        if language:
            query = query.filter(language_code=language)

        for translation_object in query:
            try:
                target_pk = CourseRunTranslation.objects.filter(
                    master=self,
                    language_code=translation_object.language_code
                ).values_list("pk", flat=True)[0]
            except IndexError:
                translation_object.pk = None
            else:
                translation_object.pk = target_pk
            translation_object.master = self
            translation_object.save()

    def mark_course_dirty(self):
        """
        Mark the related course page as dirty if the course run has changed since it was last
        published, so that the modifications can be checked and confirmed by a reviewer.
        """
        try:
            public_instance = self.__class__.objects.get(
                draft_course_run__pk=self.pk)
        except self.__class__.DoesNotExist:
            # This is a new instance, mark page dirty in all languages unless
            # the course run is yet to be scheduled (hidden from public page in this case)
            if self.state["priority"] < CourseState.TO_BE_SCHEDULED:
                self.direct_course.extended_object.title_set.update(
                    publisher_state=PUBLISHER_STATE_DIRTY)
            return

        is_visible = (
            self.state["priority"] < CourseState.TO_BE_SCHEDULED
            or public_instance.state["priority"] < CourseState.TO_BE_SCHEDULED)
        # Mark the related course page dirty if the course run content has changed
        # Break out of the for loop as soon as we found a difference
        for field in self._meta.fields:
            if field.name == "direct_course":
                if (public_instance.direct_course.draft_extension !=
                        self.direct_course and is_visible):
                    self.direct_course.extended_object.title_set.update(
                        publisher_state=PUBLISHER_STATE_DIRTY
                    )  # mark target page dirty in all languages
                    page = public_instance.direct_course.draft_extension.extended_object
                    page.title_set.update(
                        publisher_state=PUBLISHER_STATE_DIRTY
                    )  # mark source page dirty in all languages
                    break
            elif (field.editable and not field.auto_created and getattr(
                    public_instance, field.name) != getattr(self, field.name)
                  and is_visible):
                self.direct_course.extended_object.title_set.update(
                    publisher_state=PUBLISHER_STATE_DIRTY
                )  # mark page dirty in all languages
                break

    def save(self, *args, **kwargs):
        """Enforce validation each time an instance is saved."""
        self.full_clean()
        super().save(*args, **kwargs)

    # pylint: disable=signature-differs
    def delete(self, *args, **kwargs):
        """
        Mark the related course page as dirty if the course about to be deleted was
        published and visible (not to be scheduled).
        """
        try:
            # pylint: disable=no-member
            public_course_run = self.public_course_run
        except CourseRun.DoesNotExist:
            pass
        else:
            if public_course_run.state[
                    "priority"] < CourseState.TO_BE_SCHEDULED:
                self.direct_course.extended_object.title_set.update(
                    publisher_state=PUBLISHER_STATE_DIRTY
                )  # mark page dirty in all languages
        return super().delete(*args, **kwargs)

    # pylint: disable=too-many-return-statements
    @staticmethod
    def compute_state(start, end, enrollment_start, enrollment_end):
        """
        Compute at the current time the state of a course run that would have the dates
        passed in argument.

        A static method not using the instance allows to call it with an Elasticsearch result.
        """
        if not start or not enrollment_start:
            return CourseState(CourseState.TO_BE_SCHEDULED)

        # course run end dates are not required and should default to forever
        # e.g. a course run with no end date is presumed to be always on-going
        end = end or MAX_DATE
        enrollment_end = enrollment_end or MAX_DATE

        now = timezone.now()
        if start < now:
            if end > now:
                if enrollment_end > now:
                    # ongoing open
                    return CourseState(CourseState.ONGOING_OPEN,
                                       enrollment_end)
                # ongoing closed
                return CourseState(CourseState.ONGOING_CLOSED)
            if enrollment_start < now < enrollment_end:
                # archived open
                return CourseState(CourseState.ARCHIVED_OPEN, enrollment_end)
            # archived closed
            return CourseState(CourseState.ARCHIVED_CLOSED)
        if enrollment_start > now:
            # future not yet open
            return CourseState(CourseState.FUTURE_NOT_YET_OPEN, start)
        if enrollment_end > now:
            # future open
            return CourseState(CourseState.FUTURE_OPEN, start)
        # future already closed
        return CourseState(CourseState.FUTURE_CLOSED)

    @property
    def state(self):
        """Return the state of the course run at the current time."""
        return self.compute_state(self.start, self.end, self.enrollment_start,
                                  self.enrollment_end)

    def get_course(self):
        """Get the course for this course run."""
        is_draft = self.direct_course.extended_object.publisher_is_draft
        ancestor_nodes = self.direct_course.extended_object.node.get_ancestors(
        )
        return Course.objects.filter(
            # Joining on `cms_pages` generate duplicates for courses that are under a parent page
            # when this page exists both in draft and public versions. We need to exclude the
            # parent public page to avoid this duplication
            Q(extended_object__node__cms_pages__publisher_is_draft=is_draft
              )  # course has a parent
            | Q(extended_object__node__isnull=True),  # course has no parent
            # Target courses that are ancestors of the course related to the course run
            Q(id=self.direct_course_id)
            | Q(extended_object__node__in=ancestor_nodes),
            # Exclude snapshots
            extended_object__node__parent__cms_pages__course__isnull=
            True,  # exclude snapshots
            # Get the course in the same version as the course run
            extended_object__publisher_is_draft=is_draft,
        ).distinct()[0]

    @property
    def safe_title(self):
        """
        Access the `title` translatable field from the `CourseRunTranslation` on a safe way.
        """
        try:
            return self.title
        except ObjectDoesNotExist:
            return None
예제 #16
0
파일: _units.py 프로젝트: samyka/E-Commerce
class SalesUnit(_ShortNameToSymbol, TranslatableE-CommerceModel):
    identifier = InternalIdentifierField(unique=True)
    decimals = models.PositiveSmallIntegerField(default=0, verbose_name=_(u"allowed decimal places"), help_text=_(
        "The number of decimal places allowed by this sales unit."
        "Set this to a value greater than zero if products with this sales unit can be sold in fractional quantities"
    ))

    name = TranslatedField()
    symbol = TranslatedField()

    class Meta:
        verbose_name = _('sales unit')
        verbose_name_plural = _('sales units')

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

    @property
    def allow_fractions(self):
        return self.decimals > 0

    @cached_property
    def quantity_step(self):
        """
        Get the quantity increment for the amount of decimals this unit allows.

        For 0 decimals, this will be 1; for 1 decimal, 0.1; etc.

        :return: Decimal in (0..1]
        :rtype: Decimal
        """

        # This particular syntax (`10 ^ -n`) is the same that `bankers_round` uses
        # to figure out the quantizer.

        return Decimal(10) ** (-int(self.decimals))

    def round(self, value):
        return bankers_round(parse_decimal_string(value), self.decimals)

    @property
    def display_unit(self):
        """
        Default display unit of this sales unit.

        Get a `DisplayUnit` object, which has this sales unit as its
        internal unit and is marked as a default, or if there is no
        default display unit for this sales unit, then a proxy object.
        The proxy object has the same display unit interface and mirrors
        the properties of the sales unit, such as symbol and decimals.

        :rtype: DisplayUnit
        """
        cache_key = "display_unit:sales_unit_{}_default_display_unit".format(self.pk)
        default_display_unit = cache.get(cache_key)

        if default_display_unit is None:
            default_display_unit = self.display_units.filter(default=True).first()
            # Set 0 to cache to prevent None values, which will not be a valid cache value
            # 0 will be invalid below, hence we prevent another query here
            cache.set(cache_key, default_display_unit or 0)

        return default_display_unit or SalesUnitAsDisplayUnit(self)
예제 #17
0
class Service(TranslatableShoopModel):
    """
    Abstract base model for services.

    Each enabled service should be linked to a service provider and
    should have a choice identifier specified in its `choice_identifier`
    field.  The choice identifier should be valid for the service
    provider, i.e. it should be one of the `ServiceChoice.identifier`
    values returned by the `ServiceProvider.get_service_choices` method.
    """
    identifier = InternalIdentifierField(unique=True,
                                         verbose_name=_("identifier"))
    enabled = models.BooleanField(default=False, verbose_name=_("enabled"))
    shop = models.ForeignKey(Shop, verbose_name=_("shop"))

    choice_identifier = models.CharField(blank=True,
                                         max_length=64,
                                         verbose_name=_("choice identifier"))

    # These are for migrating old methods to new architecture
    old_module_identifier = models.CharField(max_length=64, blank=True)
    old_module_data = JSONField(blank=True, null=True)

    name = TranslatedField(any_language=True)
    description = TranslatedField()
    logo = FilerImageField(blank=True,
                           null=True,
                           on_delete=models.SET_NULL,
                           verbose_name=_("logo"))
    tax_class = models.ForeignKey('TaxClass',
                                  on_delete=models.PROTECT,
                                  verbose_name=_("tax class"))

    behavior_components = models.ManyToManyField(
        'ServiceBehaviorComponent', verbose_name=_("behavior components"))

    objects = ServiceQuerySet.as_manager()

    class Meta:
        abstract = True

    @property
    def provider(self):
        """
        :rtype: shoop.core.models.ServiceProvider
        """
        return getattr(self, self.provider_attr)

    def get_checkout_phase(self, **kwargs):
        """
        :rtype: shoop.core.front.checkout.CheckoutPhaseViewMixin|None
        """
        return self.provider.get_checkout_phase(service=self, **kwargs)

    def get_effective_name(self, source):
        """
        Get effective name of the service for given order source.

        By default, effective name is the same as name of this service,
        but if there is a service provider with a custom implementation
        for `~shoop.core.models.ServiceProvider.get_effective_name`
        method, then this can be different.

        :type source: shoop.core.order_creator.OrderSource
        :rtype: str
        """
        if not self.provider:
            return self.name
        return self.provider.get_effective_name(self, source)

    def is_available_for(self, source):
        """
        Return true if service is available for given source.

        :type source: shoop.core.order_creator.OrderSource
        :rtype: bool
        """
        return not any(self.get_unavailability_reasons(source))

    def get_unavailability_reasons(self, source):
        """
        Get reasons of being unavailable for given source.

        :type source: shoop.core.order_creator.OrderSource
        :rtype: Iterable[ValidationError]
        """
        if not self.provider or not self.provider.enabled or not self.enabled:
            yield ValidationError(_("%s is disabled") % self, code='disabled')

        if source.shop != self.shop:
            yield ValidationError(_("%s is for different shop") % self,
                                  code='wrong_shop')

        for component in self.behavior_components.all():
            for reason in component.get_unavailability_reasons(self, source):
                yield reason

    def get_total_cost(self, source):
        """
        Get total cost of this service for items in given source.

        :type source: shoop.core.order_creator.OrderSource
        :rtype: PriceInfo
        """
        return _sum_costs(self.get_costs(source), source)

    def get_costs(self, source):
        """
        Get costs of this service for items in given source.

        :type source: shoop.core.order_creator.OrderSource
        :return: description, price and tax class of the costs
        :rtype: Iterable[ServiceCost]
        """
        for component in self.behavior_components.all():
            for cost in component.get_costs(self, source):
                yield cost

    def get_lines(self, source):
        """
        Get lines for given source.

        Lines are created based on costs.  Costs without description are
        combined to single line.

        :type source: shoop.core.order_creator.OrderSource
        :rtype: Iterable[shoop.core.order_creator.SourceLine]
        """
        for (num, line_data) in enumerate(self._get_line_data(source), 1):
            (price_info, tax_class, text) = line_data
            yield self._create_line(source, num, price_info, tax_class, text)

    def _get_line_data(self, source):
        # Split to costs with and without description
        costs_with_description = []
        costs_without_description = []
        for cost in self.get_costs(source):
            if cost.description:
                costs_with_description.append(cost)
            else:
                assert cost.tax_class is None
                costs_without_description.append(cost)

        if not (costs_with_description or costs_without_description):
            costs_without_description = [ServiceCost(source.create_price(0))]

        effective_name = self.get_effective_name(source)

        # Yield the combined cost first
        if costs_without_description:
            combined_price_info = _sum_costs(costs_without_description, source)
            yield (combined_price_info, self.tax_class, effective_name)

        # Then the costs with description, one line for each cost
        for cost in costs_with_description:
            tax_class = (cost.tax_class or self.tax_class)
            text = _('%(service_name)s: %(sub_item)s') % {
                'service_name': effective_name,
                'sub_item': cost.description,
            }
            yield (cost.price_info, tax_class, text)

    def _create_line(self, source, num, price_info, tax_class, text):
        return source.create_line(
            line_id=self._generate_line_id(num),
            type=self.line_type,
            quantity=price_info.quantity,
            text=text,
            base_unit_price=price_info.base_unit_price,
            discount_amount=price_info.discount_amount,
            tax_class=tax_class,
        )

    def _generate_line_id(self, num):
        return "%s-%02d-%08x" % (self.line_type.name.lower(), num,
                                 random.randint(0, 0x7FFFFFFF))

    def _make_sure_is_usable(self):
        if not self.provider:
            raise ValueError('%r has no %s' % (self, self.provider_attr))
        if not self.enabled:
            raise ValueError('%r is disabled' % (self, ))
        if not self.provider.enabled:
            raise ValueError('%s of %r is disabled' %
                             (self.provider_attr, self))