def test_deconstruct(): f1 = MoneyField(currency='EUR', default=EUR(0)) name, path, args, kwargs = f1.deconstruct() f2 = MoneyField(*args, **kwargs) assert f1.currency_code == f2.currency_code assert f1.decimal_places == f2.decimal_places assert f1.default == f2.default
def test_get_default(): OneEuro = EUR(1) f = MoneyField(currency='EUR', null=True) assert f.get_default() is None f = MoneyField(currency='EUR', null=True, default=EUR()) assert f.get_default() == EUR() f = MoneyField(currency='EUR', null=False, default=OneEuro) assert f.get_default() == OneEuro
class SmartCard(Product): # common product fields unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) # product properties CARD_TYPE = (2 * ('{}{}'.format(s, t),) for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')) card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15) SPEED = ((str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280)) speed = models.CharField(_("Transfer Speed"), choices=SPEED, max_length=8) product_code = models.CharField(_("Product code"), max_length=255, unique=True) storage = models.PositiveIntegerField(_("Storage Capacity"), help_text=_("Storage capacity in GB")) multilingual = TranslatedFields(description=HTMLField(verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_("Full description used in the catalog's detail view of Smart Cards."))) class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") def get_price(self, request): return self.unit_price def get_product_variant(self, extra): """ SmartCards do not have flavors, they are the product. """ return self
class SmartCard(CMSPageReferenceMixin, TranslatableModelMixin, BaseProduct): product_name = models.CharField(max_length=255, verbose_name=_("Product Name")) slug = models.SlugField(verbose_name=_("Slug")) unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) description = TranslatedField() # product properties manufacturer = models.ForeignKey(Manufacturer, verbose_name=_("Manufacturer")) CARD_TYPE = (2 * ('{}{}'.format(s, t), ) for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')) card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15) SPEED = ((str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280)) speed = models.CharField(_("Transfer Speed"), choices=SPEED, max_length=8) product_code = models.CharField(_("Product code"), max_length=255, unique=True) storage = models.PositiveIntegerField( _("Storage Capacity"), help_text=_("Storage capacity in GB")) # controlling the catalog order = models.PositiveIntegerField(verbose_name=_("Sort by"), db_index=True) cms_pages = models.ManyToManyField( 'cms.Page', through=ProductPage, help_text=_("Choose list view this product shall appear on.")) images = models.ManyToManyField('filer.Image', through=ProductImage) objects = ProductManager() # filter expression used to lookup for a product item using the Select2 widget lookup_fields = ( 'product_code__startswith', 'product_name__icontains', ) class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") ordering = ('order', ) objects = ProductManager() def __str__(self): return self.product_name @property def sample_image(self): return self.images.first() def get_price(self, request): return self.unit_price
class OrderPayment(with_metaclass(deferred.ForeignKeyBuilder, models.Model)): """ A model to hold received payments for a given order. """ order = deferred.ForeignKey( BaseOrder, verbose_name=_("Order"), ) amount = MoneyField( _("Amount paid"), help_text=_("How much was paid with this particular transfer."), ) transaction_id = models.CharField( _("Transaction ID"), max_length=255, help_text=_("The transaction processor's reference"), ) created_at = models.DateTimeField( _("Received at"), auto_now_add=True, ) payment_method = models.CharField( _("Payment method"), max_length=50, help_text=_("The payment backend used to process the purchase"), ) class Meta: verbose_name = pgettext_lazy('order_models', "Order payment") verbose_name_plural = pgettext_lazy('order_models', "Order payments")
class KhimageVariant(models.Model): product = models.ForeignKey( KhimageModel, verbose_name=_("Khimage Model"), related_name='variants', ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) storage = models.PositiveIntegerField( _("Internal Storage"), help_text=_("Internal storage in MB"), ) def get_price(self, request): return self.unit_price
class Commodity(Product): """ This Commodity model inherits from polymorphic Product, and therefore has to be redefined. """ unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) # controlling the catalog placeholder = PlaceholderField("Commodity Details") show_breadcrumb = True # hard coded to always show the product's breadcrumb default_manager = TranslatableManager() class Meta: verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def get_price(self, request): return self.unit_price
class Membership(Product): """ This Commodity model inherits from polymorphic Product, and therefore has to be redefined. """ unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) signup_date = models.DateField( _("Signed Up"), default=timezone.now, ) profile = models.ForeignKey(User, null=True, blank=True) # controlling the catalog placeholder = PlaceholderField("Membership Details") show_breadcrumb = True # hard coded to always show the product's breadcrumb class Meta: verbose_name = _("Membership") def get_price(self, request): return self.unit_price
class Commodity(AvailableProductMixin, Product): """ This Commodity model inherits from polymorphic Product, and therefore has to be redefined. """ unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock")) # controlling the catalog placeholder = PlaceholderField("Commodity Details") show_breadcrumb = True # hard coded to always show the product's breadcrumb class Meta: verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def get_price(self, request): return self.unit_price
class SmartCard(Product): # common product fields unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) # product properties CARD_TYPE = (2 * ('{}{}'.format(s, t),) for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')) card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15) SPEED = ((str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280)) speed = models.CharField(_("Transfer Speed"), choices=SPEED, max_length=8) product_code = models.CharField(_("Product code"), max_length=255, unique=True) storage = models.PositiveIntegerField(_("Storage Capacity"), help_text=_("Storage capacity in GB")) class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") def get_price(self, request): return self.unit_price def get_product_markedness(self, extra): """ SmartCards do not have a markedness, they are the product. """ return self
class SmartCard(AvailableProductMixin, Product): multilingual = TranslatedFields( description=HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_( "Full description used in the catalog's detail view of Smart Cards."), ), ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) card_type = models.CharField( _("Card Type"), choices=[2 * ('{}{}'.format(s, t),) for t in ['SD', 'SDXC', 'SDHC', 'SDHC II'] for s in ['', 'micro ']], max_length=15, ) speed = models.CharField( _("Transfer Speed"), choices=[(str(s), "{} MB/s".format(s)) for s in [4, 20, 30, 40, 48, 80, 95, 280]], max_length=8, ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) storage = models.PositiveIntegerField( _("Storage Capacity"), help_text=_("Storage capacity in GB"), ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock") ) class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") ordering = ['order'] # filter expression used to lookup for a product item using the Select2 widget lookup_fields = ['product_code__startswith', 'product_name__icontains'] def get_price(self, request): return self.unit_price default_manager = ProductManager()
def test_to_python(): f = MoneyField(currency='EUR', null=True) assert f.to_python(3) == EUR('3') assert f.to_python('3.14') == EUR('3.14') assert f.to_python(None) == EUR() assert f.to_python(EUR(3)) == EUR('3') with pytest.raises(ValidationError): f.to_python('abc')
class Commodity(CMSPageReferenceMixin, TranslatableModelMixin, BaseProduct): """ Generic Product Commodity to be used whenever the merchant does not require product specific attributes and just required a placeholder field to add arbitrary data. """ # common product fields product_code = models.CharField(_("Product code"), max_length=255, unique=True) unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) # controlling the catalog order = models.PositiveIntegerField(verbose_name=_("Sort by"), db_index=True) cms_pages = models.ManyToManyField( 'cms.Page', through=ProductPage, help_text=_("Choose list view this product shall appear on.")) sample_image = image.FilerImageField( verbose_name=_("Sample Image"), blank=True, null=True, help_text=_("Sample image used in the catalog's list view.")) show_breadcrumb = models.BooleanField( _("Show Breadcrumb"), default=True, help_text=_( "Shall the detail page show the product's breadcrumb.")) placeholder = PlaceholderField("Commodity Details") # translatable fields for the catalog's list- and detail views product_name = TranslatedField() slug = TranslatedField() caption = TranslatedField() # filter expression used to search for a product item using the Select2 widget lookup_fields = ( 'product_code__startswith', 'product_name__icontains', ) objects = ProductManager() class Meta: app_label = app_settings.APP_LABEL ordering = ('order', ) verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def __str__(self): return self.product_code def get_price(self, request): return self.unit_price
class SmartCard(Product): # common product fields unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) # product properties CARD_TYPE = (2 * ('{}{}'.format(s, t), ) for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')) card_type = models.CharField( _("Card Type"), choices=CARD_TYPE, max_length=15, ) SPEED = [(str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280)] speed = models.CharField( _("Transfer Speed"), choices=SPEED, max_length=8, ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) manufacturer = models.ForeignKey( Manufacturer, default=1, verbose_name=_("Manufacturer"), ) storage = models.PositiveIntegerField( _("Storage Capacity"), help_text=_("Storage capacity in GB"), ) description = HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_( "Full description used in the catalog's detail view of Smart Cards." ), ) default_manager = BaseProductManager() class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") def get_price(self, request): return self.unit_price
class SmartPhone(models.Model): product = models.ForeignKey(SmartPhoneModel, verbose_name=_("Smart-Phone Model")) product_code = models.CharField(_("Product code"), max_length=255, unique=True) unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) storage = models.PositiveIntegerField(_("Internal Storage"), help_text=_("Internal storage in MB")) def get_price(self, request): return self.unit_price
class ShippingDestination(models.Model): shipping_method = models.ForeignKey( ShippingMethod, related_name='destinations', ) country = models.CharField(max_length=3) price = MoneyField(currency='EUR') class Meta: verbose_name = _("Shipping Destination") verbose_name_plural = _("Shipping Destination") unique_together = ['country', 'shipping_method']
class ScooterVariant(models.Model): product = models.ForeignKey( ScooterModel, verbose_name=_("Scooter Model"), related_name='variants', ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) #~ storage = models.PositiveIntegerField( #~ _("Internal Storage"), #~ help_text=_("Internal storage in MB"), #~ ) #~ battery_capacity = models.DecimalField( #~ _("Battery capacity"), #~ max_digits=3, #~ decimal_places=1, #~ help_text=_("Battery capacity in Ah"), #~ ) COLORS = ( ("white", "white"), ("black", "black"), ) color = models.CharField( _("Color"), max_length=255, choices=COLORS, default='white', ) def get_price(self, request): return self.unit_price
class Commodity(Product): # common product fields unit_price = MoneyField(_("Unit price"), decimal_places=3, help_text=_("Net price for this product")) product_code = models.CharField(_("Product code"), max_length=255, unique=True) # controlling the catalog placeholder = PlaceholderField("Commodity Details") class Meta: verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def get_price(self, request): return self.unit_price
class Commodity(AvailableProductMixin, Product, TranslatableModelMixin): """ This Commodity model inherits from polymorphic Product, and therefore has to be redefined. """ unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) product_code = models.ForeignKey( ProductList, on_delete=models.CASCADE, ) multilingual = TranslatedFields( description=HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', blank=True, help_text= _("Full description used in the catalog's detail view of Smart Cards." ), ), caption=HTMLField( verbose_name=_("Caption"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', blank=True, help_text= _("Full description used in the catalog's detail view of Smart Cards." ), ), ) # controlling the catalog placeholder = PlaceholderField("Commodity Details") show_breadcrumb = True # hard coded to always show the product's breadcrumb default_manager = TranslatableManager() class Meta: verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def get_price(self, request): return self.unit_price
class SmartPhoneVariant(AvailableProductMixin, models.Model): product = models.ForeignKey( SmartPhoneModel, on_delete=models.CASCADE, verbose_name=_("Smartphone Model"), related_name='variants', ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) storage = models.PositiveIntegerField( _("Internal Storage"), help_text=_("Internal storage in GB"), ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock")) def __str__(self): return _("{product} with {storage} GB").format(product=self.product, storage=self.storage) def get_price(self, request): return self.unit_price
class Scoop(Product): # common product fields unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) # product properties product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) time_duration = models.PositiveIntegerField( _("time duration"), help_text=_("time in days"), ) description = HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_( "Full description used in the catalog's detail view of Scoop."), ) default_manager = BaseProductManager() class Meta: verbose_name = _("Scoop") verbose_name_plural = _("Scoop") def get_price(self, request): return self.unit_price
class SofaVariant(models.Model): class Meta: ordering = ('unit_price', ) product_model = models.ForeignKey( SofaModel, related_name='variants', on_delete=models.CASCADE, ) product_code = models.ForeignKey( ProductList, on_delete=models.CASCADE, blank=True, ) images = models.ManyToManyField( 'filer.Image', through='VariantImage', ) unit_price = MoneyField(_("Unit price"), blank=True) fabric = models.ForeignKey( Fabric, on_delete=models.CASCADE, blank=True, ) def get_availability(self, request, **kwargs): return True def __str__(self): return self.fabric.fabric_name def delete(self, using=None, keep_parents=False): ProductList.objects.filter(product_code=self.product_code).delete() super(SofaVariant, self).delete()
class Fabric(Product): fabric_name = models.CharField( _("Fabric name"), max_length=150, blank=True, ) # common product fields unit_price = MoneyField( _("Price per meter"), decimal_places=2, help_text=_("Net price for this product by meter"), ) # product properties FABRIC_TYPE = [ ('leath', 'leather'), ('velv', 'velvet'), ('wool', 'wool'), ] fabric_type = models.CharField( _("Fabric type"), choices=FABRIC_TYPE, max_length=15, ) product_code = models.ForeignKey( ProductList, on_delete=models.CASCADE, blank=True, ) composition = models.CharField( _("Comosition of fabric"), max_length=255, unique=False, ) care = models.CharField( _("Recommended wash care"), max_length=255, unique=False, ) multilingual = TranslatedFields( description=HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', blank=True, help_text= _("Full description used in the catalog's detail view of Smart Cards." ), ), caption=HTMLField( verbose_name=_("Caption"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', blank=True, help_text= _("Full description used in the catalog's detail view of Smart Cards." ), ), ) default_manager = ProductManager() class Meta: verbose_name = _("Fabric") verbose_name_plural = _("Fabrics") def get_price(self, request): return self.unit_price
class WeltladenProduct(CMSPageReferenceMixin, TranslatableModelMixin, BaseProduct): product_name = models.CharField( max_length=255, verbose_name=_("Product Name"), ) slug = models.SlugField(verbose_name=_("Slug")) caption = TranslatedField() short_description = TranslatedField() description = TranslatedField() ingredients = TranslatedField() # product properties manufacturer = models.ForeignKey( Manufacturer, on_delete=models.CASCADE, verbose_name=_("Manufacturer"), blank=True, null=True, ) additional_manufacturers = models.ManyToManyField( Manufacturer, blank=True, verbose_name=_("Additional Manufacturers"), related_name="additional_manufacturers", ) display_manufacturer_as_raw_material_supplier = models.BooleanField( _("Display manufacturer as raw material supplier"), default=False) supplier = models.ForeignKey(Supplier, verbose_name=_("Supplier"), on_delete=models.CASCADE) quality_labels = models.ManyToManyField(QualityLabel, blank=True, verbose_name=_("Quality labels"), related_name="quality_labels") origin_countries = CountryField( verbose_name=_("Origin countries"), blank_label=_('Select one or many'), multiple=True, blank=True, ) # controlling the catalog order = models.PositiveIntegerField( _("Sort by"), db_index=True, ) cms_pages = models.ManyToManyField( 'cms.Page', through=ProductPage, help_text=_("Choose list view this product shall appear on."), ) images = models.ManyToManyField( 'filer.Image', through=ProductImage, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Gross price for this product"), ) vegan = models.BooleanField(_("Vegan"), default=False) lactose_free = models.BooleanField(_("Lactose free"), default=False) gluten_free = models.BooleanField(_("Gluten free"), default=False) tax_switch = models.BooleanField( _("Switch Tax"), default=True, help_text=_( "If switched on, then 20% tax item, if off then 10% tax item")) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) instagram_category = models.ForeignKey(InstagramCategory, on_delete=models.CASCADE, null=True, blank=True) weight = models.DecimalField( _("Weight"), help_text=_("Weight in kilograms (kg). max 99.99kg"), decimal_places=2, max_digits=4, default=0.0) class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") ordering = ['order'] # filter expression used to lookup for a product item using the Select2 widget lookup_fields = ['product_code__startswith', 'product_name__icontains'] def get_price(self, request): return self.unit_price objects = ProductManager() def __str__(self): return self.product_name @property def ordered_quality_labels(self): return self.quality_labels.all().order_by('ordering') @property def sample_image(self): return self.images.first() def get_weight(self): return self.weight def invalidate_cache(self): """ Method ``ProductCommonSerializer.render_html()`` caches the rendered HTML snippets. Invalidate this HTML snippet after changing relevant parts of the product. """ shop_app = apps.get_app_config('shop') if shop_app.cache_supporting_wildcard: cache.delete('product:{}|*'.format(self.id)) def clean_fields(self, exclude=None): super().clean_fields(exclude=exclude) if WeltladenProduct.objects.filter(slug=self.slug).exclude( id=self.id).exists(): raise ValidationError(_('Product slug already exits'), code='invalid')
class Product(BaseProduct, TranslatableModel): """ Product model. """ SINGLE, GROUP, VARIANT = range(3) KINDS = ( (SINGLE, _('Single')), (GROUP, _('Group')), (VARIANT, _('Variant')), ) translations = TranslatedFields( name=models.CharField( _('Name'), max_length=128, ), slug=models.SlugField( _('Slug'), db_index=True, help_text= _("Part that's used in url to display this product. Needs to be unique." ), ), _caption=models.TextField( _('Caption'), max_length=255, blank=True, help_text= _("Short product caption, usually used in catalog's list view of products." ), ), _description=models.TextField( _('Description'), blank=True, help_text= _("Description of a product, usually used as lead text in product's detail view." ), ), meta={ 'unique_together': [('language_code', 'slug')], }, ) code = models.CharField( _('Code'), max_length=64, unique=True, help_text=_('Unique identifier for a product.'), ) # Categorization _category = TreeForeignKey( Category, models.CASCADE, blank=True, null=True, verbose_name=_('Category'), ) _brand = TreeForeignKey( Brand, models.CASCADE, blank=True, null=True, verbose_name=_('Brand'), ) _manufacturer = TreeForeignKey( Manufacturer, models.CASCADE, blank=True, null=True, verbose_name=_('Manufacturer'), ) # Pricing _unit_price = MoneyField( _('Unit price'), default=0, help_text=_("For variants leave empty to use the Group price."), ) _discount = models.DecimalField( _('Discount %'), blank=True, null=True, max_digits=4, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))], help_text=_("For variants leave empty to use Group discount."), ) _tax = models.ForeignKey( Tax, models.SET_NULL, blank=True, null=True, verbose_name=_('Tax'), help_text=_( "Tax to be applied to this product. Variants inherit tax percentage from their Group, and should " "leave this field empty."), ) # Settings kind = models.PositiveSmallIntegerField( _('Kind'), choices=KINDS, default=SINGLE, help_text= _('Choose a product type. Single products are products without variations. Group products are base products ' 'that hold variants and their common info, they cannot be added to cart. Variants are variations of a ' 'product that must select a Group product, and set their unique set of attributes. ' '(See "Variant" section below)'), ) discountable = models.BooleanField( _('Discountable'), default=True, help_text=_('Can this product be used in an offer?'), ) modifiers = models.ManyToManyField( Modifier, blank=True, verbose_name=_('Modifiers'), limit_choices_to={'kind__in': [Modifier.STANDARD, Modifier.DISCOUNT]}, ) flags = models.ManyToManyField( Flag, blank=True, verbose_name=_('Flags'), help_text=_('Check flags for this product.'), ) # Measurements _width = MeasurementField( _('Width'), blank=True, null=True, measurement=Distance, ) _height = MeasurementField( _('Height'), blank=True, null=True, measurement=Distance, ) _depth = MeasurementField( _('Depth'), blank=True, null=True, measurement=Distance, ) _weight = MeasurementField( _('Weight'), blank=True, null=True, measurement=Mass, ) # Group available_attributes = models.ManyToManyField( 'Attribute', blank=True, related_name='products_available_attributes', verbose_name=_('Attributes'), help_text=_( 'Select attributes that can be used in a Variant for this product.' ), ) # Variant group = models.ForeignKey( 'self', models.CASCADE, blank=True, null=True, related_name='variants', verbose_name=_('Group'), help_text=_('Select a Group product for this variation.'), ) attributes = models.ManyToManyField( 'Attribute', through='AttributeValue', verbose_name=_('Attributes'), ) published = models.DateTimeField( _('Published'), default=timezone.now, ) quantity = models.IntegerField( _('Quantity'), blank=True, null=True, help_text=_( 'Number of available products to ship. Leave empty if product is always available, or set to 0 if product ' 'is not available.'), ) order = models.BigIntegerField( _('Sort'), default=0, ) content = PlaceholderField('shopit_product_content') objects = ProductManager() lookup_fields = ['code__startswith', 'translations__name__icontains'] class Meta: db_table = 'shopit_products' verbose_name = _('Product') verbose_name_plural = _('Products') ordering = ['-order'] def __str__(self): return self.product_name def save(self, *args, **kwargs): """ Clean and clear product. Set unique ordering value for product and it's variants based on published timestamp. Force Single groups to a Group kind. """ self.clean() self.clear() if self.is_variant: self.group.clear('_variants', '_invalid_variants', '_variations', '_attribute_choices', '_combinations') self.order = self.group.order if self.group.is_single: self.group.kind = Product.GROUP self.group.save(update_fields=['kind']) super(Product, self).save(*args, **kwargs) else: # Don't generate timestamp if published hasn't changed. if self.pk is not None: original = Product.objects.get( pk=self.pk).published.strftime('%s') if original == self.published.strftime('%s'): super(Product, self).save(*args, **kwargs) return timestamp = int(self.published.strftime('%s')) same = Product.objects.filter(order__startswith=timestamp).first() timestamp = same.order + 1 if same else timestamp * 1000 self.order = timestamp super(Product, self).save(*args, **kwargs) if self.is_group: for variant in self.get_variants(): variant.clear() variant.order = timestamp variant.save(update_fields=['order']) def get_absolute_url(self, language=None): if not language: language = get_current_language() with switch_language(self, language): try: return reverse('shopit-product-detail', args=[self.safe_translation_getter('slug')]) except NoReverseMatch: # pragma: no cover pass @property def product_name(self): return self.safe_translation_getter( 'name', any_language=True) if self.pk else '' @property def product_code(self): return self.code @property def is_single(self): return self.kind == self.SINGLE @property def is_group(self): return self.kind == self.GROUP @property def is_variant(self): return self.kind == self.VARIANT @property def caption(self): return self.get_attr('_caption', '', translated=True) @caption.setter def caption(self, value): self._caption = value @property def description(self): return self.get_attr('_description', '', translated=True) @description.setter def description(self, value): self._description = value @property def category(self): return self.get_attr('_category') @category.setter def category(self, value): self._category = value @property def brand(self): return self.get_attr('_brand') @brand.setter def brand(self, value): self._brand = value @property def manufacturer(self): return self.get_attr('_manufacturer') @manufacturer.setter def manufacturer(self, value): self._manufacturer = value @property def unit_price(self): return self.get_attr('_unit_price', 0) @unit_price.setter def unit_price(self, value): self._unit_price = value @property def discount(self): return self.get_attr('_discount') @discount.setter def discount(self, value): self._discount = value @property def tax(self): return self.get_attr('_tax') or getattr(self.category, 'tax', None) @tax.setter def tax(self, value): self._tax = value @property def width(self): return self.get_attr('_width') @width.setter def width(self, value): self._width = value if isinstance(value, Distance) else Distance( m=value) @property def height(self): return self.get_attr('_height') @height.setter def height(self, value): self._height = value if isinstance(value, Distance) else Distance( m=value) @property def depth(self): return self.get_attr('_depth') @depth.setter def depth(self, value): self._depth = value if isinstance(value, Distance) else Distance( m=value) @property def weight(self): return self.get_attr('_weight') @weight.setter def weight(self, value): self._weight = value if isinstance(value, Mass) else Mass(g=value) @property def price(self): return self.get_price() @property def is_discounted(self): return bool(self.discount_percent) @property def is_taxed(self): return bool(self.tax_percent) @property def discount_percent(self): return self.discount or Decimal('0.00') @property def tax_percent(self): return getattr(self.tax, 'percent', Decimal('0.00')) @property def discount_amount(self): return self.unit_price * self.discount_percent / 100 @property def tax_amount(self): return (self.unit_price - self.discount_amount) * self.tax_percent / 100 @property def primary_image(self): return self.images.first() @cached_property def images(self): images = self.attachments.filter(kind=Attachment.IMAGE) return images or (self.group.images if self.is_variant else images) @cached_property def videos(self): videos = self.attachments.filter(kind=Attachment.VIDEO) return videos or (self.group.videos if self.is_variant else videos) @cached_property def files(self): files = self.attachments.filter(kind=Attachment.FILE) return files or (self.group.files if self.is_variant else files) def get_price(self, request=None): """ Returns price with discount and tax calculated. """ return self.unit_price - self.discount_amount + self.tax_amount def get_availability(self, request=None): """ Returns product availibility as list of tuples `(quantity, until)` Method is not yet implemented in django-shop. """ availability = self.quantity if self.quantity is not None else True if self.is_group: availability = 0 elif self.is_variant and self not in self.group.get_variants(): availability = 0 return [(availability, datetime.max)] def is_available(self, quantity=1, request=None): """ Returns if product is available for the given quantity. If request is passed in, count items already in cart. """ if request: cart = Cart.objects.get_or_create_from_request(request) cart_item = self.is_in_cart(cart) quantity += cart_item.quantity if cart_item else 0 now = timezone.now().replace(tzinfo=None) number, until = self.get_availability(request)[0] number = 100000 if number is True else number available = number >= quantity and now < until return available, int(number - quantity) def get_modifiers(self, distinct=True): """ Returns all modifiers for this product. Collects categorization and group modifiers. """ mods = getattr(self, '_mods', None) if mods is None: mods = self.modifiers.active() if self.is_variant: mods = mods | self.group.get_modifiers(distinct=False) else: if self.category: mods = mods | self.category.get_modifiers(distinct=False) if self.brand: mods = mods | self.brand.get_modifiers(distinct=False) if self.manufacturer: mods = mods | self.manufacturer.get_modifiers( distinct=False) self.cache('_mods', mods) return mods.distinct() if distinct else mods def get_flags(self, distinct=True): """ Returns all flags for this product. Collects categorization and group flags. """ flags = getattr(self, '_flags', None) if flags is None: flags = self.flags.active() if self.is_variant: flags = flags | self.group.get_flags(distinct=False) if self.category: flags = flags | self.category.get_flags(distinct=False) if self.brand: flags = flags | self.brand.get_flags(distinct=False) if self.manufacturer: flags = flags | self.manufacturer.get_flags(distinct=False) self.cache('_flags', flags) return flags.distinct() if distinct else flags def get_available_attributes(self): """ Returns list of available attributes for Group and Variant products. """ if self.is_group: if not hasattr(self, '_available_attributes'): self.cache('_available_attributes', self.available_attributes.active()) return getattr(self, '_available_attributes') def get_attributes(self): """ Returns a dictionary containing Variant attributes. """ if self.is_variant: attrs = getattr(self, '_attributes', OrderedDict()) if not attrs: for value in self.attribute_values.select_related('attribute'): attrs[value.attribute.key] = value.as_dict self.cache('_attributes', attrs) return attrs def get_variants(self): """ Returns valid variants of a Group product. """ if self.is_group: variants = getattr(self, '_variants', None) if variants is None: variants = self.variants.all() invalid = [x.pk for x in self.get_invalid_variants()] variants = self.variants.exclude(pk__in=invalid) self.cache('_variants', variants) return variants def get_invalid_variants(self): """ Returns variants that whose attributes don't match available attributes and they need to be re-configured or deleted. """ if self.is_group: if not hasattr(self, '_invalid_variants'): invalid = [] valid_attrs = [ ] # Keep track of valid attrs to check for duplicates. codes = sorted(self.get_available_attributes().values_list( 'code', flat=True)) for variant in self.variants.all(): attrs = variant.get_attributes() if (attrs in valid_attrs or sorted(x['code'] for x in attrs.values()) != codes or True in [ not x['nullable'] and x['value'] == '' for x in attrs.values() ]): invalid.append(variant) else: valid_attrs.append(attrs) self.cache('_invalid_variants', invalid) return getattr(self, '_invalid_variants') def get_variations(self): """ Returns a list of tuples containing a variant id and it's attributes. """ if self.is_group: if not hasattr(self, '_variations'): variations = [(x.pk, x.get_attributes()) for x in self.get_variants()] self.cache('_variations', variations) return getattr(self, '_variations') def get_attribute_choices(self): """ Returns available attribute choices for a group product, filtering only the used ones. Used to display dropdown fields on a group product to select a variant. """ if self.is_group: if not hasattr(self, '_attribute_choices'): used = [ tuple([y['code'], y['value']]) for x in self.get_variations() for y in x[1].values() ] attrs = OrderedDict() for attr in self.get_available_attributes(): data = attr.as_dict data['choices'] = [ x.as_dict for x in attr.get_choices() if (x.attribute.code, x.value) in used ] if data['choices']: attrs[attr.code] = data self.cache('_attribute_choices', attrs) return getattr(self, '_attribute_choices') def get_combinations(self): """ Returns all available Variant combinations for a Group product based on `Available attributes` field, replacing the existant variants with actual variant data. Variants with attributes missing or not specified in `Available attributes` will not be included. This is used to show possible combinations in admin, as well as creating them automatically. """ if self.is_group: if not hasattr(self, '_combinations'): values = [] for attr in self.get_available_attributes(): vals = [ AttributeValue(attribute=attr, choice=x) for x in attr.get_choices() ] if vals: values.append(vals) combinations = [] if values: for combo in itertools.product(*values): attrs = OrderedDict([(x.attribute.code, x.value) for x in combo]) name = self.safe_translation_getter('name', any_language=True) name = '%s %s' % (name, ' '.join( [x.label for x in combo if x.label != '-'])) name = name.rstrip() slug = slugify(name) languages = [] variant = self.get_variant(attrs) if variant: name = variant.safe_translation_getter( 'name', name) slug = variant.safe_translation_getter( 'slug', slug) languages = variant.get_available_languages() combinations.append({ 'pk': variant.pk if variant else None, 'name': name, 'slug': slug, 'code': variant.code if variant else None, 'price': variant.get_price() if variant else None, 'quantity': variant.quantity if variant else None, 'languages': languages, 'attributes': OrderedDict([(x.attribute.code, x.as_dict) for x in combo]) }) self.cache('_combinations', combinations) return getattr(self, '_combinations') def get_attachments(self): """ Returns all attachments as a dictionary. If Product is a Variant and has not attachments itself, group attachemts are inherited. """ attachments = getattr(self, '_attachments', None) if attachments is None: attachments = { 'images': [x.as_dict for x in self.images] or None, 'videos': [x.as_dict for x in self.videos] or None, 'files': [x.as_dict for x in self.files] or None, } self.cache('_attachments', attachments) return attachments def get_relations(self): """ Returns relations for a Single or Group product. """ if not self.is_variant: if not hasattr(self, '_relations'): self.cache('_relations', self.relations.all()) return getattr(self, '_relations') def get_related_products(self, kind=None): """ Returns related products with the given kind. Variants inherit their related products from a Group. """ if self.is_variant: return self.group.get_related_products(kind) relations = self.get_relations() if kind is not None: relations = relations.filter(kind=kind) return [x.product for x in relations] def get_reviews(self, language=None, include_inactive=False): """ Returns reviews for this product, uses the group product for varaints. """ if not self.is_variant: reviews = getattr(self, '_reviews', None) if include_inactive: reviews = self.reviews.all() if reviews is None: reviews = self.reviews.active() self.cache('_reviews', reviews) if language is not None: return reviews.filter(language=language) return reviews def get_variant(self, attrs): """ Returns a Variant with the given attribute values for this Group. eg. attrs = {'code': 'value', 'code2': 'value2'} """ if self.is_group: for variant in self.get_variants(): current = [(x['code'], x['value']) for x in variant.get_attributes().values()] if (sorted(attrs.items()) == sorted(current)): return variant def filter_variants(self, attrs): """ Returns a list of Variant products for this Group that contain attribute values passed in as `attrs`. eg. attrs = {'code': 'value', 'code2': 'value2'} """ if self.is_group: variants = [] for variant in self.get_variants(): valid = True current = [(x['code'], x['value']) for x in variant.get_attributes().values()] for attr in attrs.items(): if attr not in current: valid = False break if valid: variants.append(variant) return variants def create_variant(self, combo, language=None): """ Create a variant with the given `combo` object from the `get_combinations` method. """ if self.is_group: if not language: language = get_current_language() slug = combo['slug'] code = combo['code'] or Product.objects.latest('pk').pk + 1 num = 0 while Product.objects.translated(slug=slug).exists(): num = num + 1 slug = '%s-%d' % (combo['slug'], num) while Product.objects.filter( code=code, translations__language_code=language).exists(): code = int(code) + 1 variant, created = Product.objects.get_or_create( code=code, kind=Product.VARIANT, group=self) variant.set_current_language(language) variant.name = combo['name'] variant.slug = slug variant.save() if created: for attr_value in combo['attributes'].values(): attr = Attribute.objects.get(code=attr_value['code']) if attr_value['value'] == '' and attr.nullable: choice = None else: choice = attr.choices.get(pk=attr_value['choice']) AttributeValue.objects.create(attribute=attr, product=variant, choice=choice) return variant @method_decorator(transaction.atomic) def create_all_variants(self, language=None): """ Creates all missing variants for the group. """ if self.is_group: variants = [] if not language: language = get_current_language() for combo in self.get_combinations(): if not combo['pk'] or language not in combo['languages']: variants.append( self.create_variant(combo, language=language)) return variants def get_attr(self, name, case=None, translated=False):
class Commodity(CMSPageReferenceMixin, CommodityMixin, BaseProduct): """ Generic Product Commodity to be used whenever the merchant does not require product specific attributes and just required a placeholder field to add arbitrary data. """ # common product fields product_name = models.CharField( max_length=255, verbose_name=_("Product Name"), ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) # controlling the catalog order = models.PositiveIntegerField( verbose_name=_("Sort by"), db_index=True, ) cms_pages = models.ManyToManyField( 'cms.Page', through=ProductPage, help_text=_("Choose list view this product shall appear on."), ) sample_image = image.FilerImageField( verbose_name=_("Sample Image"), blank=True, null=True, default=None, on_delete=models.SET_DEFAULT, help_text=_("Sample image used in the catalog's list view."), ) show_breadcrumb = models.BooleanField( _("Show Breadcrumb"), default=True, help_text=_( "Shall the detail page show the product's breadcrumb."), ) placeholder = PlaceholderField("Commodity Details") quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock")) # common fields for the catalog's list- and detail views slug = models.SlugField(verbose_name=_("Slug")) caption = HTMLField( verbose_name=_("Caption"), blank=True, null=True, help_text=_("Short description for the catalog list view."), ) # filter expression used to search for a product item using the Select2 widget lookup_fields = ['product_code__startswith', 'product_name__icontains'] objects = BaseProductManager() class Meta: app_label = app_settings.APP_LABEL ordering = ('order', ) verbose_name = _("Commodity") verbose_name_plural = _("Commodities") def __str__(self): return self.product_code
class LamellaFix(Product): # common product fields unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_(" per unit"), ) LAM_WIDTH = (('38', '38 mm'), ('53', '53 mm'), ('63', '63 mm'), ('68', '68 mm')) lamella_width = models.CharField( _('width'), default=53, max_length=6, choices=LAM_WIDTH, help_text=_("Lammelas width") ) length = models.CharField( _("Lamella's length"), max_length=25, blank=True, ) depth = models.CharField( _("Lamella's depth"), max_length=25, blank=True, ) weight = models.CharField( _('weight'), default=0, max_length=6, help_text=_("Weight of item, kg") ) is_lamella = models.BooleanField( _("Lamella"), default=True, help_text=_("Is this lamella (for calculating weight)."), ) weight_by_hand = models.BooleanField( _("Enter weight by hand"), default=False, help_text=_("For enter lamella weight by hand"), ) discont_scheme = models.ForeignKey(Discount, blank=True, on_delete=models.CASCADE) product_code = models.CharField( _("Product code"), max_length=255, blank=True, ) description = HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_("Full description used in the catalog's detail view of Smart Cards."), ) default_manager = BaseProductManager() class Meta: verbose_name = _("Lamella") verbose_name_plural = _("Lamellas") def is_unique_scu(self, scu): scu = str(scu) try: LamellaFix.objects.get(product_code=scu) return False except: return True def set_num_scu(self, scu, n): scu = str(scu) while len(scu) <= int(n): scu = '0'+ scu return scu def get_max_scu(self): codes = LamellaFix.objects.all() max_scu = 0 for code in codes: code = int(code.product_code) if code > max_scu: max_scu = code return max_scu def save(self, *args, **kwargs): if not self.product_code or not self.is_unique_scu(self.product_code): max_scu = int(self.get_max_scu()) while True: new_scu = max_scu + 1 if self.is_unique_scu(new_scu): self.product_code = self.set_num_scu(new_scu, 4) break # TODO: unique product_code if(self.is_lamella and not self.weight_by_hand): m = 0.00075 # calculated empiric method vol = float(self.length) * float(self.lamella_width) * float(self.depth) self.weight = round((vol * m / 1000), 3) super(LamellaFix, self).save(*args, **kwargs) def get_price(self, request): return self.unit_price def get_rebate(self, x): some_str = self.discont_scheme.discont_scheme.split("\r\n") x = int(x) temp_discont = 0 for i in some_str: discont = i.split(":") num = int(discont[0]) #get first element of tuple for get quantity in db if x>=num: temp_discont = int(discont[1]) elif x<=num: return temp_discont return temp_discont
def test_from_db_value(): f = MoneyField(currency='EUR', null=True) assert f.from_db_value(Decimal('3'), None, None) == EUR('3') assert f.from_db_value(3.45, None, None) == EUR('3.45') assert f.from_db_value(None, None, None) is None
def test_get_prep_value(): f = MoneyField(currency='EUR', null=True) assert f.get_prep_value(EUR('3')) == Decimal('3')
class Modifier(TranslatableModel): STANDARD = 'standard' DISCOUNT = 'discount' CART = 'cart' KINDS = ( (STANDARD, _('Standard')), (DISCOUNT, _('Discount')), (CART, _('Cart')), ) translations = TranslatedFields(name=models.CharField( _('Name'), max_length=128, ), ) code = models.SlugField( _('Code'), unique=True, help_text=_('Unique identifier for this modifier.'), ) amount = MoneyField( _('Amount'), default=0, help_text=('Amount that should be added. Can be negative.'), ) percent = models.DecimalField( _('Percent'), blank=True, null=True, max_digits=4, decimal_places=2, help_text= _('Percent that should be added, overrides the amount. Can be negative.' ), ) kind = models.CharField( _('Kind'), max_length=16, choices=KINDS, default=STANDARD, help_text=_( 'Standard affects the product regardles, Discount checks for a "Discountable" flag on a product and ' 'should be negative, Cart will affect an entire cart.'), ) active = models.BooleanField( _('Active'), default=True, help_text=_('Is this modifier publicly visible.'), ) created_at = models.DateTimeField( _('Created at'), auto_now_add=True, ) updated_at = models.DateTimeField( _('Updated at'), auto_now=True, ) order = models.PositiveIntegerField( _('Sort'), default=0, ) objects = ModifierQuerySet.as_manager() class Meta: db_table = 'shopit_modifiers' verbose_name = _('Modifier') verbose_name_plural = _('Modifiers') ordering = ['order'] def __str__(self): return self.label def save(self, *args, **kwargs): self.clean() super(Modifier, self).save(*args, **kwargs) @property def label(self): return self.safe_translation_getter('name', any_language=True) @property def requires_code(self): return self.discount_codes.active().exists() @property def is_filtering_enabled(self): return Modifier.objects.filtering_enabled().active().filter( id=self.id).exists() def get_conditions(self): if not hasattr(self, '_conditions'): setattr(self, '_conditions', list(self.conditions.all())) return getattr(self, '_conditions') def get_discount_codes(self, include_added=False): key = '_discount_codes_added' if include_added else '_discount_codes' if not hasattr(self, key): setattr( self, key, list(self.discount_codes.valid(include_added=include_added))) return getattr(self, key) def get_added_amount(self, price, quantity=1): return self.percent * price / 100 if self.percent else self.amount * quantity def can_be_applied(self, request, cart_item=None, cart=None): """ Returns if a modifier can be applied to the given cart or cart item. Either `cart_item` or `cart` must be passed in. """ if cart_item is None and cart is None: return False if cart_item and not self.is_eligible_product(cart_item.product): return False for condition in self.get_conditions(): if not condition.is_met(request, cart_item, cart): return False if self.requires_code and not self.is_code_applied( cart_item.cart_id if cart_item else cart.id): return False return self.active # Should never happen to be False up to this point, but just in case. def is_eligible_product(self, product): """ Returns if modifier can be applied to the given product. """ if self.kind == self.DISCOUNT: return product.discountable return self.kind == self.STANDARD def is_code_applied(self, cart_id): """ Make sure that at least one code is applied to the given cart. """ cart_codes = CartDiscountCode.objects.filter( cart_id=cart_id).values_list('code', flat=True) for code in self.get_discount_codes(include_added=True): if code.code in cart_codes: return True return False def clean(self): if self.kind == self.DISCOUNT: if self.percent and self.percent >= 0 or not self.percent and self.amount >= 0: raise ValidationError(em('discount_not_negative')) @classmethod def get_cart_modifiers(cls): return cls.objects.filter(kind=cls.CART)