We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Test update_pack command
Created in #864

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `shopelectro/models.py`
Content:
```
1 import enum
2 import random
3 import string
4 import typing
5 from uuid import uuid4
6 
7 from django.conf import settings
8 from django.db import models
9 from django.urls import reverse
10 from django.utils.translation import ugettext_lazy as _
11 
12 from catalog import models as catalog_models
13 from ecommerce import models as ecommerce_models
14 from pages import models as pages_models
15 
16 
17 def randomize_slug(slug: str) -> str:
18     slug_hash = ''.join(
19         random.choices(string.ascii_lowercase, k=settings.SLUG_HASH_SIZE)
20     )
21     return f'{slug}_{slug_hash}'
22 
23 
24 class SECategoryQuerySet(catalog_models.CategoryQuerySet):
25     def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet':
26         categories_with_pictures = (
27             self
28             .filter(products__page__images__isnull=False)
29             .distinct()
30         )
31 
32         return categories_with_pictures.get_ancestors(include_self=True)
33 
34 
35 class SECategoryManager(
36     catalog_models.CategoryManager.from_queryset(SECategoryQuerySet)
37 ):
38     pass
39 
40 
41 class Category(catalog_models.AbstractCategory, pages_models.SyncPageMixin):
42 
43     objects = SECategoryManager()
44     uuid = models.UUIDField(default=uuid4, editable=False)
45 
46     @classmethod
47     def get_default_parent(cls):
48         return pages_models.CustomPage.objects.filter(slug='catalog').first()
49 
50     @property
51     def image(self):
52         products = self.products.all()
53         return products[0].image if products else None
54 
55     def get_absolute_url(self):
56         return reverse('category', args=(self.page.slug,))
57 
58 
59 class Product(
60     catalog_models.AbstractProduct,
61     catalog_models.AbstractPosition,
62     pages_models.SyncPageMixin
63 ):
64 
65     # That's why we are needed to explicitly add objects manager here
66     # because of Django special managers behaviour.
67     # Se se#480 for details.
68     objects = catalog_models.ProductManager()
69 
70     category = models.ForeignKey(
71         Category,
72         on_delete=models.CASCADE,
73         null=True,
74         related_name='products',
75         verbose_name=_('category'),
76     )
77 
78     tags = models.ManyToManyField(
79         'Tag',
80         related_name='products',
81         blank=True,
82         verbose_name=_('tags'),
83     )
84 
85     vendor_code = models.SmallIntegerField(verbose_name=_('vendor_code'))
86     uuid = models.UUIDField(default=uuid4, editable=False)
87     purchase_price = models.FloatField(
88         default=0, verbose_name=_('purchase_price'))
89     wholesale_small = models.FloatField(
90         default=0, verbose_name=_('wholesale_small'))
91     wholesale_medium = models.FloatField(
92         default=0, verbose_name=_('wholesale_medium'))
93     wholesale_large = models.FloatField(
94         default=0, verbose_name=_('wholesale_large'))
95 
96     in_pack = models.PositiveSmallIntegerField(
97         default=1,
98         verbose_name=_('in pack'),
99     )
100 
101     def get_absolute_url(self):
102         return reverse('product', args=(self.vendor_code,))
103 
104     @property
105     def average_rate(self):
106         """Return rounded to first decimal averaged rating."""
107         rating = self.product_feedbacks.aggregate(
108             avg=models.Avg('rating')).get('avg', 0)
109         return round(rating, 1)
110 
111     @property
112     def feedback_count(self):
113         return self.product_feedbacks.count()
114 
115     @property
116     def feedback(self):
117         return self.product_feedbacks.all().order_by('-date')
118 
119     def get_params(self):
120         return Tag.objects.filter_by_products([self]).group_tags()
121 
122     def get_brand_name(self) -> str:
123         brand: typing.Optional['Tag'] = Tag.objects.get_brands([self]).get(self)
124         return brand.name if brand else ''
125 
126 
127 class ProductFeedback(models.Model):
128     product = models.ForeignKey(
129         Product, on_delete=models.CASCADE, null=True,
130         related_name='product_feedbacks'
131     )
132 
133     date = models.DateTimeField(
134         auto_now=True, db_index=True, verbose_name=_('date'))
135     name = models.CharField(
136         max_length=255, db_index=True, verbose_name=_('name'))
137     rating = models.PositiveSmallIntegerField(
138         default=1, db_index=True, verbose_name=_('rating'))
139     dignities = models.TextField(
140         default='', blank=True, verbose_name=_('dignities'))
141     limitations = models.TextField(
142         default='', blank=True, verbose_name=_('limitations'))
143     general = models.TextField(
144         default='', blank=True, verbose_name=_('limitations'))
145 
146 
147 class ItemsEnum(enum.EnumMeta):
148     """
149     Provide dict-like `items` method.
150 
151     https://docs.python.org/3/library/enum.html#enum-classes
152     """
153 
154     def items(self):
155         return [(i.name, i.value) for i in self]
156 
157     def __repr__(self):
158         fields = ', '.join(i.name for i in self)
159         return f"<enum '{self.__name__}: {fields}'>"
160 
161 
162 class PaymentOptions(enum.Enum, metaclass=ItemsEnum):
163     cash = 'Наличные'
164     cashless = 'Безналичные и денежные переводы'
165     AC = 'Банковская карта'
166     PC = 'Яндекс.Деньги'
167     GP = 'Связной (терминал)'
168     AB = 'Альфа-Клик'
169 
170     @staticmethod
171     def default():
172         return PaymentOptions.cash
173 
174 
175 class Order(ecommerce_models.Order):
176     address = models.TextField(blank=True, default='')
177     payment_type = models.CharField(
178         max_length=255,
179         choices=PaymentOptions.items(),
180         default=PaymentOptions.default().name,
181     )
182     comment = models.TextField(blank=True, default='')
183     # total price - total purchase price
184     revenue = models.FloatField(default=0, null=True, verbose_name=_('revenue'))
185 
186     @property
187     def payment_type_label(self):
188         """Return label for an order's payment option."""
189         return PaymentOptions[self.payment_type].value
190 
191     def set_positions(self, cart):
192         """
193         Save cart's state into Order instance.
194 
195         @todo #589:60m Create Cart model.
196          See details here: https://github.com/fidals/shopelectro/pull/590#discussion_r222544672
197         """
198         self.revenue = cart.total_revenue()
199         self.save()
200         for id_, position in cart:
201             self.positions.create(
202                 order=self,
203                 product_id=id_,
204                 vendor_code=position['vendor_code'],
205                 name=position['name'],
206                 price=position['price'],
207                 quantity=position['quantity'],
208             )
209         return self
210 
211 
212 class CategoryPage(pages_models.ModelPage):
213     """Create proxy model for Admin."""
214 
215     class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)
216         proxy = True
217 
218     # noinspection PyTypeChecker
219     objects = pages_models.ModelPage.create_model_page_managers(Category)
220 
221 
222 class ProductPage(pages_models.ModelPage):
223     """Create proxy model for Admin."""
224 
225     class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)
226         proxy = True
227 
228     # noinspection PyTypeChecker
229     objects = (
230         pages_models.ModelPage
231         .create_model_page_managers(Product)
232     )
233 
234 
235 class TagGroup(catalog_models.TagGroup):
236     pass
237 
238 
239 class TagQuerySet(catalog_models.TagQuerySet):
240 
241     def products(self):
242         ids = self.values_list('products__id', flat=True)
243         return Product.objects.filter(id__in=ids).distinct()
244 
245 
246 class TagManager(catalog_models.TagManager.from_queryset(TagQuerySet)):
247     pass
248 
249 
250 class Tag(catalog_models.Tag):
251     group = models.ForeignKey(
252         TagGroup, on_delete=models.CASCADE, null=True, related_name='tags',
253     )
254 
255     objects = TagManager()
256
```

Path: `shopelectro/management/commands/_update_catalog/update_pack.py`
Content:
```
1 """
2 Update Product.in_pack and prices.
3 
4 The update_catalog command always resets product prices to per unit format, so:
5 1. Parse in pack quantity from Tag.name and save it to Product.in_pack
6 2. Multiply product prices by in_pack value and save.
7 """
8 import logging
9 import typing
10 
11 from django.conf import settings
12 from django.db import models, transaction
13 
14 from catalog.models_expressions import Substring
15 
16 from shopelectro.models import TagQuerySet, TagGroup
17 
18 logger = logging.getLogger(__name__)
19 PRICES = ['price', 'purchase_price', 'wholesale_small', 'wholesale_medium', 'wholesale_large']
20 
21 
22 def find_pack_group() -> typing.Optional[TagGroup]:
23     pack_group = TagGroup.objects.filter(uuid=settings.PACK_GROUP_UUID).first()
24 
25     # @todo #864:60m Raise errors in find_pack_group.
26     #  Remove Optional type as returning value and test find_pack_group.
27     if not pack_group:
28         logger.error(
29             f'Couldn\'t find "{settings.PACK_GROUP_NAME}" tag group by'
30             f'UUID="{settings.PACK_GROUP_UUID}".\n'
31             'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID.'
32         )
33         pack_group = None
34     if not settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():
35         logger.error(
36             'The pack group name isn\'t matched with the set name:'
37             f' Pack group name: {pack_group.name}\n'
38             f' Set name: {settings.PACK_GROUP_NAME}\n'
39             'Update the PACK_GROUP_NAME django settings variable to set the new relevant name.'
40         )
41         pack_group = None
42 
43     return pack_group
44 
45 
46 def update_in_packs(packs: TagQuerySet):
47     """Parse and save in pack quantity values."""
48     packs = (
49         packs
50         .annotate(
51             in_pack_str=Substring(
52                 models.F('name'),
53                 models.Value('[0-9]+\+?[0-9]*')))
54         .exclude(in_pack_str__exact='')
55     )
56 
57     for pack in packs:
58         in_pack = sum(map(int, pack.in_pack_str.split('+')))
59         pack.products.all().update(in_pack=max(in_pack, 1))
60 
61 
62 def update_prices(packs: TagQuerySet):
63     """Multiply product prices on in pack quantity."""
64     fields_to_update = {}
65     for price in PRICES:
66         fields_to_update[price] = models.F(price) * models.F('in_pack')
67 
68     with transaction.atomic():
69         packs.products().update(**fields_to_update)
70 
71 
72 def main(*args, **kwargs):
73     pack_group = find_pack_group()
74     if not pack_group:
75         return
76 
77     return
78 
79     packs = pack_group.tags.all().prefetch_related('products')
80     update_in_packs(packs)
81     update_prices(packs)
82
```
--- 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/shopelectro/management/commands/_update_catalog/update_pack.py b/shopelectro/management/commands/_update_catalog/update_pack.py
--- a/shopelectro/management/commands/_update_catalog/update_pack.py
+++ b/shopelectro/management/commands/_update_catalog/update_pack.py
@@ -6,39 +6,34 @@
 2. Multiply product prices by in_pack value and save.
 """
 import logging
-import typing
 
 from django.conf import settings
 from django.db import models, transaction
 
 from catalog.models_expressions import Substring
 
+from shopelectro.exception import UpdateCatalogException
 from shopelectro.models import TagQuerySet, TagGroup
 
 logger = logging.getLogger(__name__)
 PRICES = ['price', 'purchase_price', 'wholesale_small', 'wholesale_medium', 'wholesale_large']
 
 
-def find_pack_group() -> typing.Optional[TagGroup]:
-    pack_group = TagGroup.objects.filter(uuid=settings.PACK_GROUP_UUID).first()
-
-    # @todo #864:60m Raise errors in find_pack_group.
-    #  Remove Optional type as returning value and test find_pack_group.
-    if not pack_group:
-        logger.error(
-            f'Couldn\'t find "{settings.PACK_GROUP_NAME}" tag group by'
-            f'UUID="{settings.PACK_GROUP_UUID}".\n'
-            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID.'
+def find_pack_group() -> TagGroup:
+    try:
+        pack_group = TagGroup.objects.get_pack()
+    except TagGroup.DoesNotExist as error:
+        raise UpdateCatalogException(
+            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID. '
+            + str(error)
         )
-        pack_group = None
-    if not settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():
-        logger.error(
+    if settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():
+        raise UpdateCatalogException(
             'The pack group name isn\'t matched with the set name:'
             f' Pack group name: {pack_group.name}\n'
             f' Set name: {settings.PACK_GROUP_NAME}\n'
             'Update the PACK_GROUP_NAME django settings variable to set the new relevant name.'
         )
-        pack_group = None
 
     return pack_group
 
@@ -70,12 +65,6 @@
 
 
 def main(*args, **kwargs):
-    pack_group = find_pack_group()
-    if not pack_group:
-        return
-
-    return
-
-    packs = pack_group.tags.all().prefetch_related('products')
+    packs = find_pack_group().tags.all().prefetch_related('products')
     update_in_packs(packs)
     update_prices(packs)
diff --git a/shopelectro/models.py b/shopelectro/models.py
--- a/shopelectro/models.py
+++ b/shopelectro/models.py
@@ -232,8 +232,15 @@
     )
 
 
+class TagGroupManager(models.Manager):
+
+    def get_pack(self):
+        return self.get_queryset().get(uuid=settings.PACK_GROUP_UUID)
+
+
 class TagGroup(catalog_models.TagGroup):
-    pass
+
+    objects = TagGroupManager()
 
 
 class TagQuerySet(catalog_models.TagQuerySet):
@@ -244,7 +251,9 @@
 
 
 class TagManager(catalog_models.TagManager.from_queryset(TagQuerySet)):
-    pass
+
+    def get_packs(self):
+        return TagGroup.objects.get_pack().tags.all()
 
 
 class Tag(catalog_models.Tag):

{"golden_diff": "diff --git a/shopelectro/management/commands/_update_catalog/update_pack.py b/shopelectro/management/commands/_update_catalog/update_pack.py\n--- a/shopelectro/management/commands/_update_catalog/update_pack.py\n+++ b/shopelectro/management/commands/_update_catalog/update_pack.py\n@@ -6,39 +6,34 @@\n 2. Multiply product prices by in_pack value and save.\n \"\"\"\n import logging\n-import typing\n \n from django.conf import settings\n from django.db import models, transaction\n \n from catalog.models_expressions import Substring\n \n+from shopelectro.exception import UpdateCatalogException\n from shopelectro.models import TagQuerySet, TagGroup\n \n logger = logging.getLogger(__name__)\n PRICES = ['price', 'purchase_price', 'wholesale_small', 'wholesale_medium', 'wholesale_large']\n \n \n-def find_pack_group() -> typing.Optional[TagGroup]:\n-    pack_group = TagGroup.objects.filter(uuid=settings.PACK_GROUP_UUID).first()\n-\n-    # @todo #864:60m Raise errors in find_pack_group.\n-    #  Remove Optional type as returning value and test find_pack_group.\n-    if not pack_group:\n-        logger.error(\n-            f'Couldn\\'t find \"{settings.PACK_GROUP_NAME}\" tag group by'\n-            f'UUID=\"{settings.PACK_GROUP_UUID}\".\\n'\n-            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID.'\n+def find_pack_group() -> TagGroup:\n+    try:\n+        pack_group = TagGroup.objects.get_pack()\n+    except TagGroup.DoesNotExist as error:\n+        raise UpdateCatalogException(\n+            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID. '\n+            + str(error)\n         )\n-        pack_group = None\n-    if not settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():\n-        logger.error(\n+    if settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():\n+        raise UpdateCatalogException(\n             'The pack group name isn\\'t matched with the set name:'\n             f' Pack group name: {pack_group.name}\\n'\n             f' Set name: {settings.PACK_GROUP_NAME}\\n'\n             'Update the PACK_GROUP_NAME django settings variable to set the new relevant name.'\n         )\n-        pack_group = None\n \n     return pack_group\n \n@@ -70,12 +65,6 @@\n \n \n def main(*args, **kwargs):\n-    pack_group = find_pack_group()\n-    if not pack_group:\n-        return\n-\n-    return\n-\n-    packs = pack_group.tags.all().prefetch_related('products')\n+    packs = find_pack_group().tags.all().prefetch_related('products')\n     update_in_packs(packs)\n     update_prices(packs)\ndiff --git a/shopelectro/models.py b/shopelectro/models.py\n--- a/shopelectro/models.py\n+++ b/shopelectro/models.py\n@@ -232,8 +232,15 @@\n     )\n \n \n+class TagGroupManager(models.Manager):\n+\n+    def get_pack(self):\n+        return self.get_queryset().get(uuid=settings.PACK_GROUP_UUID)\n+\n+\n class TagGroup(catalog_models.TagGroup):\n-    pass\n+\n+    objects = TagGroupManager()\n \n \n class TagQuerySet(catalog_models.TagQuerySet):\n@@ -244,7 +251,9 @@\n \n \n class TagManager(catalog_models.TagManager.from_queryset(TagQuerySet)):\n-    pass\n+\n+    def get_packs(self):\n+        return TagGroup.objects.get_pack().tags.all()\n \n \n class Tag(catalog_models.Tag):\n", "issue": "Test update_pack command\nCreated in #864\n", "before_files": [{"content": "import enum\nimport random\nimport string\nimport typing\nfrom uuid import uuid4\n\nfrom django.conf import settings\nfrom django.db import models\nfrom django.urls import reverse\nfrom django.utils.translation import ugettext_lazy as _\n\nfrom catalog import models as catalog_models\nfrom ecommerce import models as ecommerce_models\nfrom pages import models as pages_models\n\n\ndef randomize_slug(slug: str) -> str:\n    slug_hash = ''.join(\n        random.choices(string.ascii_lowercase, k=settings.SLUG_HASH_SIZE)\n    )\n    return f'{slug}_{slug_hash}'\n\n\nclass SECategoryQuerySet(catalog_models.CategoryQuerySet):\n    def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet':\n        categories_with_pictures = (\n            self\n            .filter(products__page__images__isnull=False)\n            .distinct()\n        )\n\n        return categories_with_pictures.get_ancestors(include_self=True)\n\n\nclass SECategoryManager(\n    catalog_models.CategoryManager.from_queryset(SECategoryQuerySet)\n):\n    pass\n\n\nclass Category(catalog_models.AbstractCategory, pages_models.SyncPageMixin):\n\n    objects = SECategoryManager()\n    uuid = models.UUIDField(default=uuid4, editable=False)\n\n    @classmethod\n    def get_default_parent(cls):\n        return pages_models.CustomPage.objects.filter(slug='catalog').first()\n\n    @property\n    def image(self):\n        products = self.products.all()\n        return products[0].image if products else None\n\n    def get_absolute_url(self):\n        return reverse('category', args=(self.page.slug,))\n\n\nclass Product(\n    catalog_models.AbstractProduct,\n    catalog_models.AbstractPosition,\n    pages_models.SyncPageMixin\n):\n\n    # That's why we are needed to explicitly add objects manager here\n    # because of Django special managers behaviour.\n    # Se se#480 for details.\n    objects = catalog_models.ProductManager()\n\n    category = models.ForeignKey(\n        Category,\n        on_delete=models.CASCADE,\n        null=True,\n        related_name='products',\n        verbose_name=_('category'),\n    )\n\n    tags = models.ManyToManyField(\n        'Tag',\n        related_name='products',\n        blank=True,\n        verbose_name=_('tags'),\n    )\n\n    vendor_code = models.SmallIntegerField(verbose_name=_('vendor_code'))\n    uuid = models.UUIDField(default=uuid4, editable=False)\n    purchase_price = models.FloatField(\n        default=0, verbose_name=_('purchase_price'))\n    wholesale_small = models.FloatField(\n        default=0, verbose_name=_('wholesale_small'))\n    wholesale_medium = models.FloatField(\n        default=0, verbose_name=_('wholesale_medium'))\n    wholesale_large = models.FloatField(\n        default=0, verbose_name=_('wholesale_large'))\n\n    in_pack = models.PositiveSmallIntegerField(\n        default=1,\n        verbose_name=_('in pack'),\n    )\n\n    def get_absolute_url(self):\n        return reverse('product', args=(self.vendor_code,))\n\n    @property\n    def average_rate(self):\n        \"\"\"Return rounded to first decimal averaged rating.\"\"\"\n        rating = self.product_feedbacks.aggregate(\n            avg=models.Avg('rating')).get('avg', 0)\n        return round(rating, 1)\n\n    @property\n    def feedback_count(self):\n        return self.product_feedbacks.count()\n\n    @property\n    def feedback(self):\n        return self.product_feedbacks.all().order_by('-date')\n\n    def get_params(self):\n        return Tag.objects.filter_by_products([self]).group_tags()\n\n    def get_brand_name(self) -> str:\n        brand: typing.Optional['Tag'] = Tag.objects.get_brands([self]).get(self)\n        return brand.name if brand else ''\n\n\nclass ProductFeedback(models.Model):\n    product = models.ForeignKey(\n        Product, on_delete=models.CASCADE, null=True,\n        related_name='product_feedbacks'\n    )\n\n    date = models.DateTimeField(\n        auto_now=True, db_index=True, verbose_name=_('date'))\n    name = models.CharField(\n        max_length=255, db_index=True, verbose_name=_('name'))\n    rating = models.PositiveSmallIntegerField(\n        default=1, db_index=True, verbose_name=_('rating'))\n    dignities = models.TextField(\n        default='', blank=True, verbose_name=_('dignities'))\n    limitations = models.TextField(\n        default='', blank=True, verbose_name=_('limitations'))\n    general = models.TextField(\n        default='', blank=True, verbose_name=_('limitations'))\n\n\nclass ItemsEnum(enum.EnumMeta):\n    \"\"\"\n    Provide dict-like `items` method.\n\n    https://docs.python.org/3/library/enum.html#enum-classes\n    \"\"\"\n\n    def items(self):\n        return [(i.name, i.value) for i in self]\n\n    def __repr__(self):\n        fields = ', '.join(i.name for i in self)\n        return f\"<enum '{self.__name__}: {fields}'>\"\n\n\nclass PaymentOptions(enum.Enum, metaclass=ItemsEnum):\n    cash = '\u041d\u0430\u043b\u0438\u0447\u043d\u044b\u0435'\n    cashless = '\u0411\u0435\u0437\u043d\u0430\u043b\u0438\u0447\u043d\u044b\u0435 \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u044b'\n    AC = '\u0411\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0430\u044f \u043a\u0430\u0440\u0442\u0430'\n    PC = '\u042f\u043d\u0434\u0435\u043a\u0441.\u0414\u0435\u043d\u044c\u0433\u0438'\n    GP = '\u0421\u0432\u044f\u0437\u043d\u043e\u0439 (\u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b)'\n    AB = '\u0410\u043b\u044c\u0444\u0430-\u041a\u043b\u0438\u043a'\n\n    @staticmethod\n    def default():\n        return PaymentOptions.cash\n\n\nclass Order(ecommerce_models.Order):\n    address = models.TextField(blank=True, default='')\n    payment_type = models.CharField(\n        max_length=255,\n        choices=PaymentOptions.items(),\n        default=PaymentOptions.default().name,\n    )\n    comment = models.TextField(blank=True, default='')\n    # total price - total purchase price\n    revenue = models.FloatField(default=0, null=True, verbose_name=_('revenue'))\n\n    @property\n    def payment_type_label(self):\n        \"\"\"Return label for an order's payment option.\"\"\"\n        return PaymentOptions[self.payment_type].value\n\n    def set_positions(self, cart):\n        \"\"\"\n        Save cart's state into Order instance.\n\n        @todo #589:60m Create Cart model.\n         See details here: https://github.com/fidals/shopelectro/pull/590#discussion_r222544672\n        \"\"\"\n        self.revenue = cart.total_revenue()\n        self.save()\n        for id_, position in cart:\n            self.positions.create(\n                order=self,\n                product_id=id_,\n                vendor_code=position['vendor_code'],\n                name=position['name'],\n                price=position['price'],\n                quantity=position['quantity'],\n            )\n        return self\n\n\nclass CategoryPage(pages_models.ModelPage):\n    \"\"\"Create proxy model for Admin.\"\"\"\n\n    class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)\n        proxy = True\n\n    # noinspection PyTypeChecker\n    objects = pages_models.ModelPage.create_model_page_managers(Category)\n\n\nclass ProductPage(pages_models.ModelPage):\n    \"\"\"Create proxy model for Admin.\"\"\"\n\n    class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)\n        proxy = True\n\n    # noinspection PyTypeChecker\n    objects = (\n        pages_models.ModelPage\n        .create_model_page_managers(Product)\n    )\n\n\nclass TagGroup(catalog_models.TagGroup):\n    pass\n\n\nclass TagQuerySet(catalog_models.TagQuerySet):\n\n    def products(self):\n        ids = self.values_list('products__id', flat=True)\n        return Product.objects.filter(id__in=ids).distinct()\n\n\nclass TagManager(catalog_models.TagManager.from_queryset(TagQuerySet)):\n    pass\n\n\nclass Tag(catalog_models.Tag):\n    group = models.ForeignKey(\n        TagGroup, on_delete=models.CASCADE, null=True, related_name='tags',\n    )\n\n    objects = TagManager()\n", "path": "shopelectro/models.py"}, {"content": "\"\"\"\nUpdate Product.in_pack and prices.\n\nThe update_catalog command always resets product prices to per unit format, so:\n1. Parse in pack quantity from Tag.name and save it to Product.in_pack\n2. Multiply product prices by in_pack value and save.\n\"\"\"\nimport logging\nimport typing\n\nfrom django.conf import settings\nfrom django.db import models, transaction\n\nfrom catalog.models_expressions import Substring\n\nfrom shopelectro.models import TagQuerySet, TagGroup\n\nlogger = logging.getLogger(__name__)\nPRICES = ['price', 'purchase_price', 'wholesale_small', 'wholesale_medium', 'wholesale_large']\n\n\ndef find_pack_group() -> typing.Optional[TagGroup]:\n    pack_group = TagGroup.objects.filter(uuid=settings.PACK_GROUP_UUID).first()\n\n    # @todo #864:60m Raise errors in find_pack_group.\n    #  Remove Optional type as returning value and test find_pack_group.\n    if not pack_group:\n        logger.error(\n            f'Couldn\\'t find \"{settings.PACK_GROUP_NAME}\" tag group by'\n            f'UUID=\"{settings.PACK_GROUP_UUID}\".\\n'\n            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID.'\n        )\n        pack_group = None\n    if not settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():\n        logger.error(\n            'The pack group name isn\\'t matched with the set name:'\n            f' Pack group name: {pack_group.name}\\n'\n            f' Set name: {settings.PACK_GROUP_NAME}\\n'\n            'Update the PACK_GROUP_NAME django settings variable to set the new relevant name.'\n        )\n        pack_group = None\n\n    return pack_group\n\n\ndef update_in_packs(packs: TagQuerySet):\n    \"\"\"Parse and save in pack quantity values.\"\"\"\n    packs = (\n        packs\n        .annotate(\n            in_pack_str=Substring(\n                models.F('name'),\n                models.Value('[0-9]+\\+?[0-9]*')))\n        .exclude(in_pack_str__exact='')\n    )\n\n    for pack in packs:\n        in_pack = sum(map(int, pack.in_pack_str.split('+')))\n        pack.products.all().update(in_pack=max(in_pack, 1))\n\n\ndef update_prices(packs: TagQuerySet):\n    \"\"\"Multiply product prices on in pack quantity.\"\"\"\n    fields_to_update = {}\n    for price in PRICES:\n        fields_to_update[price] = models.F(price) * models.F('in_pack')\n\n    with transaction.atomic():\n        packs.products().update(**fields_to_update)\n\n\ndef main(*args, **kwargs):\n    pack_group = find_pack_group()\n    if not pack_group:\n        return\n\n    return\n\n    packs = pack_group.tags.all().prefetch_related('products')\n    update_in_packs(packs)\n    update_prices(packs)\n", "path": "shopelectro/management/commands/_update_catalog/update_pack.py"}], "after_files": [{"content": "import enum\nimport random\nimport string\nimport typing\nfrom uuid import uuid4\n\nfrom django.conf import settings\nfrom django.db import models\nfrom django.urls import reverse\nfrom django.utils.translation import ugettext_lazy as _\n\nfrom catalog import models as catalog_models\nfrom ecommerce import models as ecommerce_models\nfrom pages import models as pages_models\n\n\ndef randomize_slug(slug: str) -> str:\n    slug_hash = ''.join(\n        random.choices(string.ascii_lowercase, k=settings.SLUG_HASH_SIZE)\n    )\n    return f'{slug}_{slug_hash}'\n\n\nclass SECategoryQuerySet(catalog_models.CategoryQuerySet):\n    def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet':\n        categories_with_pictures = (\n            self\n            .filter(products__page__images__isnull=False)\n            .distinct()\n        )\n\n        return categories_with_pictures.get_ancestors(include_self=True)\n\n\nclass SECategoryManager(\n    catalog_models.CategoryManager.from_queryset(SECategoryQuerySet)\n):\n    pass\n\n\nclass Category(catalog_models.AbstractCategory, pages_models.SyncPageMixin):\n\n    objects = SECategoryManager()\n    uuid = models.UUIDField(default=uuid4, editable=False)\n\n    @classmethod\n    def get_default_parent(cls):\n        return pages_models.CustomPage.objects.filter(slug='catalog').first()\n\n    @property\n    def image(self):\n        products = self.products.all()\n        return products[0].image if products else None\n\n    def get_absolute_url(self):\n        return reverse('category', args=(self.page.slug,))\n\n\nclass Product(\n    catalog_models.AbstractProduct,\n    catalog_models.AbstractPosition,\n    pages_models.SyncPageMixin\n):\n\n    # That's why we are needed to explicitly add objects manager here\n    # because of Django special managers behaviour.\n    # Se se#480 for details.\n    objects = catalog_models.ProductManager()\n\n    category = models.ForeignKey(\n        Category,\n        on_delete=models.CASCADE,\n        null=True,\n        related_name='products',\n        verbose_name=_('category'),\n    )\n\n    tags = models.ManyToManyField(\n        'Tag',\n        related_name='products',\n        blank=True,\n        verbose_name=_('tags'),\n    )\n\n    vendor_code = models.SmallIntegerField(verbose_name=_('vendor_code'))\n    uuid = models.UUIDField(default=uuid4, editable=False)\n    purchase_price = models.FloatField(\n        default=0, verbose_name=_('purchase_price'))\n    wholesale_small = models.FloatField(\n        default=0, verbose_name=_('wholesale_small'))\n    wholesale_medium = models.FloatField(\n        default=0, verbose_name=_('wholesale_medium'))\n    wholesale_large = models.FloatField(\n        default=0, verbose_name=_('wholesale_large'))\n\n    in_pack = models.PositiveSmallIntegerField(\n        default=1,\n        verbose_name=_('in pack'),\n    )\n\n    def get_absolute_url(self):\n        return reverse('product', args=(self.vendor_code,))\n\n    @property\n    def average_rate(self):\n        \"\"\"Return rounded to first decimal averaged rating.\"\"\"\n        rating = self.product_feedbacks.aggregate(\n            avg=models.Avg('rating')).get('avg', 0)\n        return round(rating, 1)\n\n    @property\n    def feedback_count(self):\n        return self.product_feedbacks.count()\n\n    @property\n    def feedback(self):\n        return self.product_feedbacks.all().order_by('-date')\n\n    def get_params(self):\n        return Tag.objects.filter_by_products([self]).group_tags()\n\n    def get_brand_name(self) -> str:\n        brand: typing.Optional['Tag'] = Tag.objects.get_brands([self]).get(self)\n        return brand.name if brand else ''\n\n\nclass ProductFeedback(models.Model):\n    product = models.ForeignKey(\n        Product, on_delete=models.CASCADE, null=True,\n        related_name='product_feedbacks'\n    )\n\n    date = models.DateTimeField(\n        auto_now=True, db_index=True, verbose_name=_('date'))\n    name = models.CharField(\n        max_length=255, db_index=True, verbose_name=_('name'))\n    rating = models.PositiveSmallIntegerField(\n        default=1, db_index=True, verbose_name=_('rating'))\n    dignities = models.TextField(\n        default='', blank=True, verbose_name=_('dignities'))\n    limitations = models.TextField(\n        default='', blank=True, verbose_name=_('limitations'))\n    general = models.TextField(\n        default='', blank=True, verbose_name=_('limitations'))\n\n\nclass ItemsEnum(enum.EnumMeta):\n    \"\"\"\n    Provide dict-like `items` method.\n\n    https://docs.python.org/3/library/enum.html#enum-classes\n    \"\"\"\n\n    def items(self):\n        return [(i.name, i.value) for i in self]\n\n    def __repr__(self):\n        fields = ', '.join(i.name for i in self)\n        return f\"<enum '{self.__name__}: {fields}'>\"\n\n\nclass PaymentOptions(enum.Enum, metaclass=ItemsEnum):\n    cash = '\u041d\u0430\u043b\u0438\u0447\u043d\u044b\u0435'\n    cashless = '\u0411\u0435\u0437\u043d\u0430\u043b\u0438\u0447\u043d\u044b\u0435 \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u044b'\n    AC = '\u0411\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0430\u044f \u043a\u0430\u0440\u0442\u0430'\n    PC = '\u042f\u043d\u0434\u0435\u043a\u0441.\u0414\u0435\u043d\u044c\u0433\u0438'\n    GP = '\u0421\u0432\u044f\u0437\u043d\u043e\u0439 (\u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b)'\n    AB = '\u0410\u043b\u044c\u0444\u0430-\u041a\u043b\u0438\u043a'\n\n    @staticmethod\n    def default():\n        return PaymentOptions.cash\n\n\nclass Order(ecommerce_models.Order):\n    address = models.TextField(blank=True, default='')\n    payment_type = models.CharField(\n        max_length=255,\n        choices=PaymentOptions.items(),\n        default=PaymentOptions.default().name,\n    )\n    comment = models.TextField(blank=True, default='')\n    # total price - total purchase price\n    revenue = models.FloatField(default=0, null=True, verbose_name=_('revenue'))\n\n    @property\n    def payment_type_label(self):\n        \"\"\"Return label for an order's payment option.\"\"\"\n        return PaymentOptions[self.payment_type].value\n\n    def set_positions(self, cart):\n        \"\"\"\n        Save cart's state into Order instance.\n\n        @todo #589:60m Create Cart model.\n         See details here: https://github.com/fidals/shopelectro/pull/590#discussion_r222544672\n        \"\"\"\n        self.revenue = cart.total_revenue()\n        self.save()\n        for id_, position in cart:\n            self.positions.create(\n                order=self,\n                product_id=id_,\n                vendor_code=position['vendor_code'],\n                name=position['name'],\n                price=position['price'],\n                quantity=position['quantity'],\n            )\n        return self\n\n\nclass CategoryPage(pages_models.ModelPage):\n    \"\"\"Create proxy model for Admin.\"\"\"\n\n    class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)\n        proxy = True\n\n    # noinspection PyTypeChecker\n    objects = pages_models.ModelPage.create_model_page_managers(Category)\n\n\nclass ProductPage(pages_models.ModelPage):\n    \"\"\"Create proxy model for Admin.\"\"\"\n\n    class Meta(pages_models.ModelPage.Meta):  # Ignore PycodestyleBear (E303)\n        proxy = True\n\n    # noinspection PyTypeChecker\n    objects = (\n        pages_models.ModelPage\n        .create_model_page_managers(Product)\n    )\n\n\nclass TagGroupManager(models.Manager):\n\n    def get_pack(self):\n        return self.get_queryset().get(uuid=settings.PACK_GROUP_UUID)\n\n\nclass TagGroup(catalog_models.TagGroup):\n\n    objects = TagGroupManager()\n\n\nclass TagQuerySet(catalog_models.TagQuerySet):\n\n    def products(self):\n        ids = self.values_list('products__id', flat=True)\n        return Product.objects.filter(id__in=ids).distinct()\n\n\nclass TagManager(catalog_models.TagManager.from_queryset(TagQuerySet)):\n\n    def get_packs(self):\n        return TagGroup.objects.get_pack().tags.all()\n\n\nclass Tag(catalog_models.Tag):\n    group = models.ForeignKey(\n        TagGroup, on_delete=models.CASCADE, null=True, related_name='tags',\n    )\n\n    objects = TagManager()\n", "path": "shopelectro/models.py"}, {"content": "\"\"\"\nUpdate Product.in_pack and prices.\n\nThe update_catalog command always resets product prices to per unit format, so:\n1. Parse in pack quantity from Tag.name and save it to Product.in_pack\n2. Multiply product prices by in_pack value and save.\n\"\"\"\nimport logging\n\nfrom django.conf import settings\nfrom django.db import models, transaction\n\nfrom catalog.models_expressions import Substring\n\nfrom shopelectro.exception import UpdateCatalogException\nfrom shopelectro.models import TagQuerySet, TagGroup\n\nlogger = logging.getLogger(__name__)\nPRICES = ['price', 'purchase_price', 'wholesale_small', 'wholesale_medium', 'wholesale_large']\n\n\ndef find_pack_group() -> TagGroup:\n    try:\n        pack_group = TagGroup.objects.get_pack()\n    except TagGroup.DoesNotExist as error:\n        raise UpdateCatalogException(\n            'Update the PACK_GROUP_UUID django settings variable to set the new relevant UUID. '\n            + str(error)\n        )\n    if settings.PACK_GROUP_NAME.lower() not in pack_group.name.lower():\n        raise UpdateCatalogException(\n            'The pack group name isn\\'t matched with the set name:'\n            f' Pack group name: {pack_group.name}\\n'\n            f' Set name: {settings.PACK_GROUP_NAME}\\n'\n            'Update the PACK_GROUP_NAME django settings variable to set the new relevant name.'\n        )\n\n    return pack_group\n\n\ndef update_in_packs(packs: TagQuerySet):\n    \"\"\"Parse and save in pack quantity values.\"\"\"\n    packs = (\n        packs\n        .annotate(\n            in_pack_str=Substring(\n                models.F('name'),\n                models.Value('[0-9]+\\+?[0-9]*')))\n        .exclude(in_pack_str__exact='')\n    )\n\n    for pack in packs:\n        in_pack = sum(map(int, pack.in_pack_str.split('+')))\n        pack.products.all().update(in_pack=max(in_pack, 1))\n\n\ndef update_prices(packs: TagQuerySet):\n    \"\"\"Multiply product prices on in pack quantity.\"\"\"\n    fields_to_update = {}\n    for price in PRICES:\n        fields_to_update[price] = models.F(price) * models.F('in_pack')\n\n    with transaction.atomic():\n        packs.products().update(**fields_to_update)\n\n\ndef main(*args, **kwargs):\n    packs = find_pack_group().tags.all().prefetch_related('products')\n    update_in_packs(packs)\n    update_prices(packs)\n", "path": "shopelectro/management/commands/_update_catalog/update_pack.py"}]}