We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Product variant without name or attrs displays as blank space in dashboard's breadcrumbs
Seems like `__str__` on our `ProductVariant` may produce empty string if ProductVariant has no name or attributes set. Our titles seem to fallback to SKU if this is the case, but not our breadcrumbs:

<img width="362" alt="zrzut ekranu 2018-03-01 o 13 58 23" src="https://user-images.githubusercontent.com/750553/36845921-d21bf616-1d58-11e8-851c-763c32356818.png">


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `saleor/product/models.py`
Content:
```
1 import datetime
2 from decimal import Decimal
3 
4 from django.conf import settings
5 from django.contrib.postgres.fields import HStoreField
6 from django.core.validators import MinValueValidator, RegexValidator
7 from django.db import models
8 from django.db.models import F, Max, Q
9 from django.urls import reverse
10 from django.utils.encoding import smart_text
11 from django.utils.text import slugify
12 from django.utils.translation import pgettext_lazy
13 from django_prices.models import MoneyField
14 from mptt.managers import TreeManager
15 from mptt.models import MPTTModel
16 from prices import Money, TaxedMoney, TaxedMoneyRange
17 from text_unidecode import unidecode
18 from versatileimagefield.fields import PPOIField, VersatileImageField
19 
20 from ..core.exceptions import InsufficientStock
21 from ..discount.utils import calculate_discounted_price
22 from ..seo.models import SeoModel
23 
24 
25 class Category(MPTTModel, SeoModel):
26     name = models.CharField(max_length=128)
27     slug = models.SlugField(max_length=128)
28     description = models.TextField(blank=True)
29     parent = models.ForeignKey(
30         'self', null=True, blank=True, related_name='children',
31         on_delete=models.CASCADE)
32     background_image = VersatileImageField(
33         upload_to='category-backgrounds', blank=True, null=True)
34 
35     objects = models.Manager()
36     tree = TreeManager()
37 
38     class Meta:
39         app_label = 'product'
40         permissions = (
41             ('view_category',
42              pgettext_lazy('Permission description', 'Can view categories')),
43             ('edit_category',
44              pgettext_lazy('Permission description', 'Can edit categories')))
45 
46     def __str__(self):
47         return self.name
48 
49     def get_absolute_url(self, ancestors=None):
50         return reverse('product:category',
51                        kwargs={'path': self.get_full_path(ancestors),
52                                'category_id': self.id})
53 
54     def get_full_path(self, ancestors=None):
55         if not self.parent_id:
56             return self.slug
57         if not ancestors:
58             ancestors = self.get_ancestors()
59         nodes = [node for node in ancestors] + [self]
60         return '/'.join([node.slug for node in nodes])
61 
62 
63 class ProductType(models.Model):
64     name = models.CharField(max_length=128)
65     has_variants = models.BooleanField(default=True)
66     product_attributes = models.ManyToManyField(
67         'ProductAttribute', related_name='product_types', blank=True)
68     variant_attributes = models.ManyToManyField(
69         'ProductAttribute', related_name='product_variant_types', blank=True)
70     is_shipping_required = models.BooleanField(default=False)
71 
72     class Meta:
73         app_label = 'product'
74 
75     def __str__(self):
76         return self.name
77 
78     def __repr__(self):
79         class_ = type(self)
80         return '<%s.%s(pk=%r, name=%r)>' % (
81             class_.__module__, class_.__name__, self.pk, self.name)
82 
83 
84 class ProductQuerySet(models.QuerySet):
85     def available_products(self):
86         today = datetime.date.today()
87         return self.filter(
88             Q(available_on__lte=today) | Q(available_on__isnull=True),
89             Q(is_published=True))
90 
91 
92 class Product(SeoModel):
93     product_type = models.ForeignKey(
94         ProductType, related_name='products', on_delete=models.CASCADE)
95     name = models.CharField(max_length=128)
96     description = models.TextField()
97     category = models.ForeignKey(
98         Category, related_name='products', on_delete=models.CASCADE)
99     price = MoneyField(
100         currency=settings.DEFAULT_CURRENCY, max_digits=12,
101         decimal_places=settings.DEFAULT_DECIMAL_PLACES)
102     available_on = models.DateField(blank=True, null=True)
103     is_published = models.BooleanField(default=True)
104     attributes = HStoreField(default={}, blank=True)
105     updated_at = models.DateTimeField(auto_now=True, null=True)
106     is_featured = models.BooleanField(default=False)
107 
108     objects = ProductQuerySet.as_manager()
109 
110     class Meta:
111         app_label = 'product'
112         permissions = (
113             ('view_product',
114              pgettext_lazy('Permission description', 'Can view products')),
115             ('edit_product',
116              pgettext_lazy('Permission description', 'Can edit products')),
117             ('view_properties',
118              pgettext_lazy(
119                  'Permission description', 'Can view product properties')),
120             ('edit_properties',
121              pgettext_lazy(
122                  'Permission description', 'Can edit product properties')))
123 
124     def __iter__(self):
125         if not hasattr(self, '__variants'):
126             setattr(self, '__variants', self.variants.all())
127         return iter(getattr(self, '__variants'))
128 
129     def __repr__(self):
130         class_ = type(self)
131         return '<%s.%s(pk=%r, name=%r)>' % (
132             class_.__module__, class_.__name__, self.pk, self.name)
133 
134     def __str__(self):
135         return self.name
136 
137     def get_absolute_url(self):
138         return reverse(
139             'product:details',
140             kwargs={'slug': self.get_slug(), 'product_id': self.id})
141 
142     def get_slug(self):
143         return slugify(smart_text(unidecode(self.name)))
144 
145     def is_in_stock(self):
146         return any(variant.is_in_stock() for variant in self)
147 
148     def is_available(self):
149         today = datetime.date.today()
150         return self.available_on is None or self.available_on <= today
151 
152     def get_first_image(self):
153         first_image = self.images.first()
154         return first_image.image if first_image else None
155 
156     def get_price_per_item(self, item, discounts=None):
157         return item.get_price_per_item(discounts)
158 
159     def get_price_range(self, discounts=None):
160         if self.variants.exists():
161             prices = [
162                 self.get_price_per_item(variant, discounts=discounts)
163                 for variant in self]
164             return TaxedMoneyRange(min(prices), max(prices))
165         price = TaxedMoney(net=self.price, gross=self.price)
166         discounted_price = calculate_discounted_price(
167             self, price, discounts)
168         return TaxedMoneyRange(start=discounted_price, stop=discounted_price)
169 
170     def get_gross_price_range(self, discounts=None):
171         grosses = [
172             self.get_price_per_item(variant, discounts=discounts)
173             for variant in self]
174         if not grosses:
175             return None
176         grosses = sorted(grosses, key=lambda x: x.tax)
177         return TaxedMoneyRange(min(grosses), max(grosses))
178 
179 
180 class ProductVariant(models.Model):
181     sku = models.CharField(max_length=32, unique=True)
182     name = models.CharField(max_length=255, blank=True)
183     price_override = MoneyField(
184         currency=settings.DEFAULT_CURRENCY, max_digits=12,
185         decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)
186     product = models.ForeignKey(
187         Product, related_name='variants', on_delete=models.CASCADE)
188     attributes = HStoreField(default={}, blank=True)
189     images = models.ManyToManyField('ProductImage', through='VariantImage')
190     quantity = models.IntegerField(
191         validators=[MinValueValidator(0)], default=Decimal(1))
192     quantity_allocated = models.IntegerField(
193         validators=[MinValueValidator(0)], default=Decimal(0))
194     cost_price = MoneyField(
195         currency=settings.DEFAULT_CURRENCY, max_digits=12,
196         decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)
197 
198     class Meta:
199         app_label = 'product'
200 
201     def __str__(self):
202         return self.name
203 
204     @property
205     def quantity_available(self):
206         return max(self.quantity - self.quantity_allocated, 0)
207 
208     def get_total(self):
209         if self.cost_price:
210             return TaxedMoney(net=self.cost_price, gross=self.cost_price)
211 
212     def check_quantity(self, quantity):
213         if quantity > self.quantity_available:
214             raise InsufficientStock(self)
215 
216     def get_price_per_item(self, discounts=None):
217         price = self.price_override or self.product.price
218         price = TaxedMoney(net=price, gross=price)
219         price = calculate_discounted_price(self.product, price, discounts)
220         return price
221 
222     def get_absolute_url(self):
223         slug = self.product.get_slug()
224         product_id = self.product.id
225         return reverse('product:details',
226                        kwargs={'slug': slug, 'product_id': product_id})
227 
228     def as_data(self):
229         return {
230             'product_name': str(self),
231             'product_id': self.product.pk,
232             'variant_id': self.pk,
233             'unit_price': str(self.get_price_per_item().gross)}
234 
235     def is_shipping_required(self):
236         return self.product.product_type.is_shipping_required
237 
238     def is_in_stock(self):
239         return self.quantity_available > 0
240 
241     def display_product(self):
242         variant_display = str(self)
243         product_display = (
244             '%s (%s)' % (self.product, variant_display)
245             if variant_display else str(self.product))
246         return smart_text(product_display)
247 
248     def get_first_image(self):
249         return self.product.get_first_image()
250 
251 
252 class ProductAttribute(models.Model):
253     slug = models.SlugField(max_length=50, unique=True)
254     name = models.CharField(max_length=100)
255 
256     class Meta:
257         ordering = ('slug', )
258 
259     def __str__(self):
260         return self.name
261 
262     def get_formfield_name(self):
263         return slugify('attribute-%s' % self.slug, allow_unicode=True)
264 
265     def has_values(self):
266         return self.values.exists()
267 
268 
269 class AttributeChoiceValue(models.Model):
270     name = models.CharField(max_length=100)
271     slug = models.SlugField(max_length=100)
272     color = models.CharField(
273         max_length=7, blank=True,
274         validators=[RegexValidator('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')])
275     attribute = models.ForeignKey(
276         ProductAttribute, related_name='values', on_delete=models.CASCADE)
277 
278     class Meta:
279         unique_together = ('name', 'attribute')
280 
281     def __str__(self):
282         return self.name
283 
284 
285 class ProductImage(models.Model):
286     product = models.ForeignKey(
287         Product, related_name='images', on_delete=models.CASCADE)
288     image = VersatileImageField(
289         upload_to='products', ppoi_field='ppoi', blank=False)
290     ppoi = PPOIField()
291     alt = models.CharField(max_length=128, blank=True)
292     order = models.PositiveIntegerField(editable=False)
293 
294     class Meta:
295         ordering = ('order', )
296         app_label = 'product'
297 
298     def get_ordering_queryset(self):
299         return self.product.images.all()
300 
301     def save(self, *args, **kwargs):
302         if self.order is None:
303             qs = self.get_ordering_queryset()
304             existing_max = qs.aggregate(Max('order'))
305             existing_max = existing_max.get('order__max')
306             self.order = 0 if existing_max is None else existing_max + 1
307         super().save(*args, **kwargs)
308 
309     def delete(self, *args, **kwargs):
310         qs = self.get_ordering_queryset()
311         qs.filter(order__gt=self.order).update(order=F('order') - 1)
312         super().delete(*args, **kwargs)
313 
314 
315 class VariantImage(models.Model):
316     variant = models.ForeignKey(
317         'ProductVariant', related_name='variant_images',
318         on_delete=models.CASCADE)
319     image = models.ForeignKey(
320         ProductImage, related_name='variant_images', on_delete=models.CASCADE)
321 
322 
323 class Collection(SeoModel):
324     name = models.CharField(max_length=128, unique=True)
325     slug = models.SlugField(max_length=128)
326     products = models.ManyToManyField(
327         Product, blank=True, related_name='collections')
328     background_image = VersatileImageField(
329         upload_to='collection-backgrounds', blank=True, null=True)
330 
331     class Meta:
332         ordering = ['pk']
333 
334     def __str__(self):
335         return self.name
336 
337     def get_absolute_url(self):
338         return reverse(
339             'product:collection',
340             kwargs={'pk': self.id, 'slug': self.slug})
341
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/saleor/product/models.py b/saleor/product/models.py
--- a/saleor/product/models.py
+++ b/saleor/product/models.py
@@ -199,7 +199,7 @@
         app_label = 'product'
 
     def __str__(self):
-        return self.name
+        return self.name or self.sku
 
     @property
     def quantity_available(self):

{"golden_diff": "diff --git a/saleor/product/models.py b/saleor/product/models.py\n--- a/saleor/product/models.py\n+++ b/saleor/product/models.py\n@@ -199,7 +199,7 @@\n         app_label = 'product'\n \n     def __str__(self):\n-        return self.name\n+        return self.name or self.sku\n \n     @property\n     def quantity_available(self):\n", "issue": "Product variant without name or attrs displays as blank space in dashboard's breadcrumbs\nSeems like `__str__` on our `ProductVariant` may produce empty string if ProductVariant has no name or attributes set. Our titles seem to fallback to SKU if this is the case, but not our breadcrumbs:\r\n\r\n<img width=\"362\" alt=\"zrzut ekranu 2018-03-01 o 13 58 23\" src=\"https://user-images.githubusercontent.com/750553/36845921-d21bf616-1d58-11e8-851c-763c32356818.png\">\r\n\n", "before_files": [{"content": "import datetime\nfrom decimal import Decimal\n\nfrom django.conf import settings\nfrom django.contrib.postgres.fields import HStoreField\nfrom django.core.validators import MinValueValidator, RegexValidator\nfrom django.db import models\nfrom django.db.models import F, Max, Q\nfrom django.urls import reverse\nfrom django.utils.encoding import smart_text\nfrom django.utils.text import slugify\nfrom django.utils.translation import pgettext_lazy\nfrom django_prices.models import MoneyField\nfrom mptt.managers import TreeManager\nfrom mptt.models import MPTTModel\nfrom prices import Money, TaxedMoney, TaxedMoneyRange\nfrom text_unidecode import unidecode\nfrom versatileimagefield.fields import PPOIField, VersatileImageField\n\nfrom ..core.exceptions import InsufficientStock\nfrom ..discount.utils import calculate_discounted_price\nfrom ..seo.models import SeoModel\n\n\nclass Category(MPTTModel, SeoModel):\n    name = models.CharField(max_length=128)\n    slug = models.SlugField(max_length=128)\n    description = models.TextField(blank=True)\n    parent = models.ForeignKey(\n        'self', null=True, blank=True, related_name='children',\n        on_delete=models.CASCADE)\n    background_image = VersatileImageField(\n        upload_to='category-backgrounds', blank=True, null=True)\n\n    objects = models.Manager()\n    tree = TreeManager()\n\n    class Meta:\n        app_label = 'product'\n        permissions = (\n            ('view_category',\n             pgettext_lazy('Permission description', 'Can view categories')),\n            ('edit_category',\n             pgettext_lazy('Permission description', 'Can edit categories')))\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self, ancestors=None):\n        return reverse('product:category',\n                       kwargs={'path': self.get_full_path(ancestors),\n                               'category_id': self.id})\n\n    def get_full_path(self, ancestors=None):\n        if not self.parent_id:\n            return self.slug\n        if not ancestors:\n            ancestors = self.get_ancestors()\n        nodes = [node for node in ancestors] + [self]\n        return '/'.join([node.slug for node in nodes])\n\n\nclass ProductType(models.Model):\n    name = models.CharField(max_length=128)\n    has_variants = models.BooleanField(default=True)\n    product_attributes = models.ManyToManyField(\n        'ProductAttribute', related_name='product_types', blank=True)\n    variant_attributes = models.ManyToManyField(\n        'ProductAttribute', related_name='product_variant_types', blank=True)\n    is_shipping_required = models.BooleanField(default=False)\n\n    class Meta:\n        app_label = 'product'\n\n    def __str__(self):\n        return self.name\n\n    def __repr__(self):\n        class_ = type(self)\n        return '<%s.%s(pk=%r, name=%r)>' % (\n            class_.__module__, class_.__name__, self.pk, self.name)\n\n\nclass ProductQuerySet(models.QuerySet):\n    def available_products(self):\n        today = datetime.date.today()\n        return self.filter(\n            Q(available_on__lte=today) | Q(available_on__isnull=True),\n            Q(is_published=True))\n\n\nclass Product(SeoModel):\n    product_type = models.ForeignKey(\n        ProductType, related_name='products', on_delete=models.CASCADE)\n    name = models.CharField(max_length=128)\n    description = models.TextField()\n    category = models.ForeignKey(\n        Category, related_name='products', on_delete=models.CASCADE)\n    price = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES)\n    available_on = models.DateField(blank=True, null=True)\n    is_published = models.BooleanField(default=True)\n    attributes = HStoreField(default={}, blank=True)\n    updated_at = models.DateTimeField(auto_now=True, null=True)\n    is_featured = models.BooleanField(default=False)\n\n    objects = ProductQuerySet.as_manager()\n\n    class Meta:\n        app_label = 'product'\n        permissions = (\n            ('view_product',\n             pgettext_lazy('Permission description', 'Can view products')),\n            ('edit_product',\n             pgettext_lazy('Permission description', 'Can edit products')),\n            ('view_properties',\n             pgettext_lazy(\n                 'Permission description', 'Can view product properties')),\n            ('edit_properties',\n             pgettext_lazy(\n                 'Permission description', 'Can edit product properties')))\n\n    def __iter__(self):\n        if not hasattr(self, '__variants'):\n            setattr(self, '__variants', self.variants.all())\n        return iter(getattr(self, '__variants'))\n\n    def __repr__(self):\n        class_ = type(self)\n        return '<%s.%s(pk=%r, name=%r)>' % (\n            class_.__module__, class_.__name__, self.pk, self.name)\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\n            'product:details',\n            kwargs={'slug': self.get_slug(), 'product_id': self.id})\n\n    def get_slug(self):\n        return slugify(smart_text(unidecode(self.name)))\n\n    def is_in_stock(self):\n        return any(variant.is_in_stock() for variant in self)\n\n    def is_available(self):\n        today = datetime.date.today()\n        return self.available_on is None or self.available_on <= today\n\n    def get_first_image(self):\n        first_image = self.images.first()\n        return first_image.image if first_image else None\n\n    def get_price_per_item(self, item, discounts=None):\n        return item.get_price_per_item(discounts)\n\n    def get_price_range(self, discounts=None):\n        if self.variants.exists():\n            prices = [\n                self.get_price_per_item(variant, discounts=discounts)\n                for variant in self]\n            return TaxedMoneyRange(min(prices), max(prices))\n        price = TaxedMoney(net=self.price, gross=self.price)\n        discounted_price = calculate_discounted_price(\n            self, price, discounts)\n        return TaxedMoneyRange(start=discounted_price, stop=discounted_price)\n\n    def get_gross_price_range(self, discounts=None):\n        grosses = [\n            self.get_price_per_item(variant, discounts=discounts)\n            for variant in self]\n        if not grosses:\n            return None\n        grosses = sorted(grosses, key=lambda x: x.tax)\n        return TaxedMoneyRange(min(grosses), max(grosses))\n\n\nclass ProductVariant(models.Model):\n    sku = models.CharField(max_length=32, unique=True)\n    name = models.CharField(max_length=255, blank=True)\n    price_override = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)\n    product = models.ForeignKey(\n        Product, related_name='variants', on_delete=models.CASCADE)\n    attributes = HStoreField(default={}, blank=True)\n    images = models.ManyToManyField('ProductImage', through='VariantImage')\n    quantity = models.IntegerField(\n        validators=[MinValueValidator(0)], default=Decimal(1))\n    quantity_allocated = models.IntegerField(\n        validators=[MinValueValidator(0)], default=Decimal(0))\n    cost_price = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)\n\n    class Meta:\n        app_label = 'product'\n\n    def __str__(self):\n        return self.name\n\n    @property\n    def quantity_available(self):\n        return max(self.quantity - self.quantity_allocated, 0)\n\n    def get_total(self):\n        if self.cost_price:\n            return TaxedMoney(net=self.cost_price, gross=self.cost_price)\n\n    def check_quantity(self, quantity):\n        if quantity > self.quantity_available:\n            raise InsufficientStock(self)\n\n    def get_price_per_item(self, discounts=None):\n        price = self.price_override or self.product.price\n        price = TaxedMoney(net=price, gross=price)\n        price = calculate_discounted_price(self.product, price, discounts)\n        return price\n\n    def get_absolute_url(self):\n        slug = self.product.get_slug()\n        product_id = self.product.id\n        return reverse('product:details',\n                       kwargs={'slug': slug, 'product_id': product_id})\n\n    def as_data(self):\n        return {\n            'product_name': str(self),\n            'product_id': self.product.pk,\n            'variant_id': self.pk,\n            'unit_price': str(self.get_price_per_item().gross)}\n\n    def is_shipping_required(self):\n        return self.product.product_type.is_shipping_required\n\n    def is_in_stock(self):\n        return self.quantity_available > 0\n\n    def display_product(self):\n        variant_display = str(self)\n        product_display = (\n            '%s (%s)' % (self.product, variant_display)\n            if variant_display else str(self.product))\n        return smart_text(product_display)\n\n    def get_first_image(self):\n        return self.product.get_first_image()\n\n\nclass ProductAttribute(models.Model):\n    slug = models.SlugField(max_length=50, unique=True)\n    name = models.CharField(max_length=100)\n\n    class Meta:\n        ordering = ('slug', )\n\n    def __str__(self):\n        return self.name\n\n    def get_formfield_name(self):\n        return slugify('attribute-%s' % self.slug, allow_unicode=True)\n\n    def has_values(self):\n        return self.values.exists()\n\n\nclass AttributeChoiceValue(models.Model):\n    name = models.CharField(max_length=100)\n    slug = models.SlugField(max_length=100)\n    color = models.CharField(\n        max_length=7, blank=True,\n        validators=[RegexValidator('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')])\n    attribute = models.ForeignKey(\n        ProductAttribute, related_name='values', on_delete=models.CASCADE)\n\n    class Meta:\n        unique_together = ('name', 'attribute')\n\n    def __str__(self):\n        return self.name\n\n\nclass ProductImage(models.Model):\n    product = models.ForeignKey(\n        Product, related_name='images', on_delete=models.CASCADE)\n    image = VersatileImageField(\n        upload_to='products', ppoi_field='ppoi', blank=False)\n    ppoi = PPOIField()\n    alt = models.CharField(max_length=128, blank=True)\n    order = models.PositiveIntegerField(editable=False)\n\n    class Meta:\n        ordering = ('order', )\n        app_label = 'product'\n\n    def get_ordering_queryset(self):\n        return self.product.images.all()\n\n    def save(self, *args, **kwargs):\n        if self.order is None:\n            qs = self.get_ordering_queryset()\n            existing_max = qs.aggregate(Max('order'))\n            existing_max = existing_max.get('order__max')\n            self.order = 0 if existing_max is None else existing_max + 1\n        super().save(*args, **kwargs)\n\n    def delete(self, *args, **kwargs):\n        qs = self.get_ordering_queryset()\n        qs.filter(order__gt=self.order).update(order=F('order') - 1)\n        super().delete(*args, **kwargs)\n\n\nclass VariantImage(models.Model):\n    variant = models.ForeignKey(\n        'ProductVariant', related_name='variant_images',\n        on_delete=models.CASCADE)\n    image = models.ForeignKey(\n        ProductImage, related_name='variant_images', on_delete=models.CASCADE)\n\n\nclass Collection(SeoModel):\n    name = models.CharField(max_length=128, unique=True)\n    slug = models.SlugField(max_length=128)\n    products = models.ManyToManyField(\n        Product, blank=True, related_name='collections')\n    background_image = VersatileImageField(\n        upload_to='collection-backgrounds', blank=True, null=True)\n\n    class Meta:\n        ordering = ['pk']\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\n            'product:collection',\n            kwargs={'pk': self.id, 'slug': self.slug})\n", "path": "saleor/product/models.py"}], "after_files": [{"content": "import datetime\nfrom decimal import Decimal\n\nfrom django.conf import settings\nfrom django.contrib.postgres.fields import HStoreField\nfrom django.core.validators import MinValueValidator, RegexValidator\nfrom django.db import models\nfrom django.db.models import F, Max, Q\nfrom django.urls import reverse\nfrom django.utils.encoding import smart_text\nfrom django.utils.text import slugify\nfrom django.utils.translation import pgettext_lazy\nfrom django_prices.models import MoneyField\nfrom mptt.managers import TreeManager\nfrom mptt.models import MPTTModel\nfrom prices import Money, TaxedMoney, TaxedMoneyRange\nfrom text_unidecode import unidecode\nfrom versatileimagefield.fields import PPOIField, VersatileImageField\n\nfrom ..core.exceptions import InsufficientStock\nfrom ..discount.utils import calculate_discounted_price\nfrom ..seo.models import SeoModel\n\n\nclass Category(MPTTModel, SeoModel):\n    name = models.CharField(max_length=128)\n    slug = models.SlugField(max_length=128)\n    description = models.TextField(blank=True)\n    parent = models.ForeignKey(\n        'self', null=True, blank=True, related_name='children',\n        on_delete=models.CASCADE)\n    background_image = VersatileImageField(\n        upload_to='category-backgrounds', blank=True, null=True)\n\n    objects = models.Manager()\n    tree = TreeManager()\n\n    class Meta:\n        app_label = 'product'\n        permissions = (\n            ('view_category',\n             pgettext_lazy('Permission description', 'Can view categories')),\n            ('edit_category',\n             pgettext_lazy('Permission description', 'Can edit categories')))\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self, ancestors=None):\n        return reverse('product:category',\n                       kwargs={'path': self.get_full_path(ancestors),\n                               'category_id': self.id})\n\n    def get_full_path(self, ancestors=None):\n        if not self.parent_id:\n            return self.slug\n        if not ancestors:\n            ancestors = self.get_ancestors()\n        nodes = [node for node in ancestors] + [self]\n        return '/'.join([node.slug for node in nodes])\n\n\nclass ProductType(models.Model):\n    name = models.CharField(max_length=128)\n    has_variants = models.BooleanField(default=True)\n    product_attributes = models.ManyToManyField(\n        'ProductAttribute', related_name='product_types', blank=True)\n    variant_attributes = models.ManyToManyField(\n        'ProductAttribute', related_name='product_variant_types', blank=True)\n    is_shipping_required = models.BooleanField(default=False)\n\n    class Meta:\n        app_label = 'product'\n\n    def __str__(self):\n        return self.name\n\n    def __repr__(self):\n        class_ = type(self)\n        return '<%s.%s(pk=%r, name=%r)>' % (\n            class_.__module__, class_.__name__, self.pk, self.name)\n\n\nclass ProductQuerySet(models.QuerySet):\n    def available_products(self):\n        today = datetime.date.today()\n        return self.filter(\n            Q(available_on__lte=today) | Q(available_on__isnull=True),\n            Q(is_published=True))\n\n\nclass Product(SeoModel):\n    product_type = models.ForeignKey(\n        ProductType, related_name='products', on_delete=models.CASCADE)\n    name = models.CharField(max_length=128)\n    description = models.TextField()\n    category = models.ForeignKey(\n        Category, related_name='products', on_delete=models.CASCADE)\n    price = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES)\n    available_on = models.DateField(blank=True, null=True)\n    is_published = models.BooleanField(default=True)\n    attributes = HStoreField(default={}, blank=True)\n    updated_at = models.DateTimeField(auto_now=True, null=True)\n    is_featured = models.BooleanField(default=False)\n\n    objects = ProductQuerySet.as_manager()\n\n    class Meta:\n        app_label = 'product'\n        permissions = (\n            ('view_product',\n             pgettext_lazy('Permission description', 'Can view products')),\n            ('edit_product',\n             pgettext_lazy('Permission description', 'Can edit products')),\n            ('view_properties',\n             pgettext_lazy(\n                 'Permission description', 'Can view product properties')),\n            ('edit_properties',\n             pgettext_lazy(\n                 'Permission description', 'Can edit product properties')))\n\n    def __iter__(self):\n        if not hasattr(self, '__variants'):\n            setattr(self, '__variants', self.variants.all())\n        return iter(getattr(self, '__variants'))\n\n    def __repr__(self):\n        class_ = type(self)\n        return '<%s.%s(pk=%r, name=%r)>' % (\n            class_.__module__, class_.__name__, self.pk, self.name)\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\n            'product:details',\n            kwargs={'slug': self.get_slug(), 'product_id': self.id})\n\n    def get_slug(self):\n        return slugify(smart_text(unidecode(self.name)))\n\n    def is_in_stock(self):\n        return any(variant.is_in_stock() for variant in self)\n\n    def is_available(self):\n        today = datetime.date.today()\n        return self.available_on is None or self.available_on <= today\n\n    def get_first_image(self):\n        first_image = self.images.first()\n        return first_image.image if first_image else None\n\n    def get_price_per_item(self, item, discounts=None):\n        return item.get_price_per_item(discounts)\n\n    def get_price_range(self, discounts=None):\n        if self.variants.exists():\n            prices = [\n                self.get_price_per_item(variant, discounts=discounts)\n                for variant in self]\n            return TaxedMoneyRange(min(prices), max(prices))\n        price = TaxedMoney(net=self.price, gross=self.price)\n        discounted_price = calculate_discounted_price(\n            self, price, discounts)\n        return TaxedMoneyRange(start=discounted_price, stop=discounted_price)\n\n    def get_gross_price_range(self, discounts=None):\n        grosses = [\n            self.get_price_per_item(variant, discounts=discounts)\n            for variant in self]\n        if not grosses:\n            return None\n        grosses = sorted(grosses, key=lambda x: x.tax)\n        return TaxedMoneyRange(min(grosses), max(grosses))\n\n\nclass ProductVariant(models.Model):\n    sku = models.CharField(max_length=32, unique=True)\n    name = models.CharField(max_length=255, blank=True)\n    price_override = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)\n    product = models.ForeignKey(\n        Product, related_name='variants', on_delete=models.CASCADE)\n    attributes = HStoreField(default={}, blank=True)\n    images = models.ManyToManyField('ProductImage', through='VariantImage')\n    quantity = models.IntegerField(\n        validators=[MinValueValidator(0)], default=Decimal(1))\n    quantity_allocated = models.IntegerField(\n        validators=[MinValueValidator(0)], default=Decimal(0))\n    cost_price = MoneyField(\n        currency=settings.DEFAULT_CURRENCY, max_digits=12,\n        decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=True, null=True)\n\n    class Meta:\n        app_label = 'product'\n\n    def __str__(self):\n        return self.name or self.sku\n\n    @property\n    def quantity_available(self):\n        return max(self.quantity - self.quantity_allocated, 0)\n\n    def get_total(self):\n        if self.cost_price:\n            return TaxedMoney(net=self.cost_price, gross=self.cost_price)\n\n    def check_quantity(self, quantity):\n        if quantity > self.quantity_available:\n            raise InsufficientStock(self)\n\n    def get_price_per_item(self, discounts=None):\n        price = self.price_override or self.product.price\n        price = TaxedMoney(net=price, gross=price)\n        price = calculate_discounted_price(self.product, price, discounts)\n        return price\n\n    def get_absolute_url(self):\n        slug = self.product.get_slug()\n        product_id = self.product.id\n        return reverse('product:details',\n                       kwargs={'slug': slug, 'product_id': product_id})\n\n    def as_data(self):\n        return {\n            'product_name': str(self),\n            'product_id': self.product.pk,\n            'variant_id': self.pk,\n            'unit_price': str(self.get_price_per_item().gross)}\n\n    def is_shipping_required(self):\n        return self.product.product_type.is_shipping_required\n\n    def is_in_stock(self):\n        return self.quantity_available > 0\n\n    def display_product(self):\n        variant_display = str(self)\n        product_display = (\n            '%s (%s)' % (self.product, variant_display)\n            if variant_display else str(self.product))\n        return smart_text(product_display)\n\n    def get_first_image(self):\n        return self.product.get_first_image()\n\n\nclass ProductAttribute(models.Model):\n    slug = models.SlugField(max_length=50, unique=True)\n    name = models.CharField(max_length=100)\n\n    class Meta:\n        ordering = ('slug', )\n\n    def __str__(self):\n        return self.name\n\n    def get_formfield_name(self):\n        return slugify('attribute-%s' % self.slug, allow_unicode=True)\n\n    def has_values(self):\n        return self.values.exists()\n\n\nclass AttributeChoiceValue(models.Model):\n    name = models.CharField(max_length=100)\n    slug = models.SlugField(max_length=100)\n    color = models.CharField(\n        max_length=7, blank=True,\n        validators=[RegexValidator('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')])\n    attribute = models.ForeignKey(\n        ProductAttribute, related_name='values', on_delete=models.CASCADE)\n\n    class Meta:\n        unique_together = ('name', 'attribute')\n\n    def __str__(self):\n        return self.name\n\n\nclass ProductImage(models.Model):\n    product = models.ForeignKey(\n        Product, related_name='images', on_delete=models.CASCADE)\n    image = VersatileImageField(\n        upload_to='products', ppoi_field='ppoi', blank=False)\n    ppoi = PPOIField()\n    alt = models.CharField(max_length=128, blank=True)\n    order = models.PositiveIntegerField(editable=False)\n\n    class Meta:\n        ordering = ('order', )\n        app_label = 'product'\n\n    def get_ordering_queryset(self):\n        return self.product.images.all()\n\n    def save(self, *args, **kwargs):\n        if self.order is None:\n            qs = self.get_ordering_queryset()\n            existing_max = qs.aggregate(Max('order'))\n            existing_max = existing_max.get('order__max')\n            self.order = 0 if existing_max is None else existing_max + 1\n        super().save(*args, **kwargs)\n\n    def delete(self, *args, **kwargs):\n        qs = self.get_ordering_queryset()\n        qs.filter(order__gt=self.order).update(order=F('order') - 1)\n        super().delete(*args, **kwargs)\n\n\nclass VariantImage(models.Model):\n    variant = models.ForeignKey(\n        'ProductVariant', related_name='variant_images',\n        on_delete=models.CASCADE)\n    image = models.ForeignKey(\n        ProductImage, related_name='variant_images', on_delete=models.CASCADE)\n\n\nclass Collection(SeoModel):\n    name = models.CharField(max_length=128, unique=True)\n    slug = models.SlugField(max_length=128)\n    products = models.ManyToManyField(\n        Product, blank=True, related_name='collections')\n    background_image = VersatileImageField(\n        upload_to='collection-backgrounds', blank=True, null=True)\n\n    class Meta:\n        ordering = ['pk']\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\n            'product:collection',\n            kwargs={'pk': self.id, 'slug': self.slug})\n", "path": "saleor/product/models.py"}]}