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
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 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 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 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 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 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 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()
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 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 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
def test_to_python(): f = MoneyDbField(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() with pytest.raises(ValidationError): f.to_python('abc')
def test_default(self): EUR = MoneyMaker("EUR") f = MoneyDbField(currency="EUR", null=False) self.assertEqual(f.get_default(), EUR()) f = MoneyDbField(currency="EUR", null=True) self.assertEqual(f.get_default(), EUR()) f = MoneyDbField(currency="EUR") self.assertEqual(f.get_default(), EUR())
def test_to_python(self): EUR = MoneyMaker("EUR") f = MoneyDbField(currency="EUR", null=True) self.assertEqual(f.to_python(3), EUR("3")) self.assertEqual(f.to_python("3.14"), EUR("3.14")) self.assertEqual(f.to_python(None), EUR()) with self.assertRaises(ValidationError): f.to_python("abc")
def test_to_python(self): EUR = MoneyMaker('EUR') f = MoneyDbField(currency='EUR', null=True) self.assertEqual(f.to_python(3), EUR('3')) self.assertEqual(f.to_python('3.14'), EUR('3.14')) self.assertEqual(f.to_python(None), EUR()) with self.assertRaises(ValidationError): f.to_python('abc')
def test_default(): OneEuro = EUR(1) f = MoneyDbField(currency='EUR', null=True) assert f.get_default() is None f = MoneyDbField(currency='EUR', null=True, default=EUR()) assert f.get_default() == EUR() f = MoneyDbField(currency='EUR', null=False, default=OneEuro) assert f.get_default() == OneEuro
def test_default(self): EUR = MoneyMaker('EUR') f = MoneyDbField(currency='EUR', null=False) self.assertEqual(f.get_default(), EUR()) f = MoneyDbField(currency='EUR', null=True) self.assertEqual(f.get_default(), EUR()) f = MoneyDbField(currency='EUR') self.assertEqual(f.get_default(), EUR())
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']
def test_default(self): EUR = MoneyMaker('EUR') OneEuro = EUR(1) f = MoneyDbField(currency='EUR', null=True) self.assertEqual(f.get_default(), None) f = MoneyDbField(currency='EUR', null=True, default=EUR()) self.assertEqual(f.get_default(), EUR()) f = MoneyDbField(currency='EUR', null=False, default=OneEuro) self.assertEqual(f.get_default(), OneEuro)
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()
def test_format(): f = MoneyDbField(max_digits=5, decimal_places=3) assert f._format(f.to_python(2)) == '2.000' assert f._format(f.to_python('2.34567')) == '2.346' assert f._format(None) is None
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')
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
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):
def test_format(self): f = MoneyDbField(max_digits=5, decimal_places=3) self.assertEqual(f._format(f.to_python(2)), '2.000') self.assertEqual(f._format(f.to_python('2.34567')), '2.346') self.assertEqual(f._format(None), None)
def test_get_prep_value(): f = MoneyField(currency='EUR', null=True) assert f.get_prep_value(EUR('3')) == Decimal('3')