Repository: fidals/shopelectro

Issue:
SE yandex.yml генерится с ошибками
На почте опять куча писем с нотисами о том, что фид генерируется с ошибками 


`Ошибка: Отсутствует обязательный элемент picture. Для каждого предложения необходимо указать как минимум один элемент picture с ссылкой на изображение товара. (строка 1169, столбец 17)
Ошибка: Отсутствует обязательный элемент picture. Для каждого предложения необходимо указать как минимум один элемент picture с ссылкой на изображение товара. (строка 1783, столбец 17)
Ошибка: Отсутствует обязательный элемент picture. Для каждого предложения необходимо указать как минимум один элемент picture с ссылкой на изображение товара. (строка 2471, столбец 17)
Ошибка: Отсутствует обязательный элемент picture. Для каждого предложения необходимо указать как минимум один элемент picture с ссылкой на изображение товара. (строка 3581, столбец 17)
и тд`


Discussion:
@duker33 
maybe an error raises becouse we do not have any `product.page.images.all` in https://github.com/fidals/shopelectro/blob/master/templates/prices/pictures.yml and we should add `if` there.
Мне кажется, что ошибка из-за того, что яндексу в каких-то категориях обязательно нужно изображение: [описание тега picture](https://yandex.ru/support/partnermarket/picture.html)
@duker33 
А есть какой-нибудь тестовый набор данных для SE, чтобы проверить генерацию фида для яндекса? Или дамп реальных, если у вас так принято. Я вижу, что есть скрипт, который стучится на рабочий сайт за бекапами, но там логин от рута, поэтому не знаю, даете вы такое новичкам, или нет
> яндексу в каких-то категориях обязательно нужно изображение

@ivnglkv , да, видимо ты прав. Бери первую картинку первого товара-наследника категории. 
Если ни одной картинки нет, юзай логотип `front/images/logo.png`

> А есть какой-нибудь тестовый набор данных для SE, чтобы проверить генерацию фида для яндекса? 

У нас есть вот такая Django fixture `shopelectro/fixtures/dump.json` для тестов на данных.
Для фидов вроде даже есть автотесты и они юзают эти данные, если я всё верно помню.
В крайнем случае можешь сгенерить руками и забросить в валидатор Яндекса
@duker33, а за нарушение пунктов
> - Недопустима ссылка на логотип магазина или «заглушку» (страницу, где написано «картинка отсутствует» и т. п.). 
> - По указанной ссылке должно размещаться изображение товара, соответствующего описанию предложения. Нельзя использовать изображение другого товара.

бан, или что-то в этом стиле не дадут, если заметят?
@ivnglkv @duker33 Можно убрать товары без картинок перечисленных категорий из yml файла и создать задачу, в которой нужно добавить в админку фильтр для товаров, которым необходимы изображения для ямаркета
@artemiy312, согласен. Определять, каким категориям обязательно нужна картинка, через спец. поле в модели `Category`? И ошибка только с фидом яндекса, остальным отбрасывать товары без картинок не надо?
@ivnglkv , нене, никаких спецполей.

У категории в плане картинок мб только два состояния: "нет картинок" и "есть картинка".
Есть картинка - это если она есть у одного из товаров-наследников категории.

И для есть/нет картинок добавляем фильтр в Админку (в отдельной задаче, конечно).

А прямо здесь просто не включай в фид удаляй категории без картинок и товары без картинок
@duker33 ок, понял. Спецполе, или какой-то другой вариант отражения в настройках я имел в виду к тому, что Яндекс не для всех категорий требует картинку у товара, следовательно какие-то товары без картинки можно оставлять
@ivnglkv , ух, какая там сложнота у Яндекса. Давай таки обойдёмся без спецполя. Чем их меньше, тем лучше для всех

Resolving patch:
diff --git a/shopelectro/management/commands/price.py b/shopelectro/management/commands/price.py
--- a/shopelectro/management/commands/price.py
+++ b/shopelectro/management/commands/price.py
@@ -67,24 +67,46 @@ def put_crumbs(product):  # Ignore PyDocStyleBear
             )
             return product
 
-        def filter_categories():
+        def filter_categories(utm):
             categories_to_exclude = (
                 Category.objects
                 .filter(name__in=cls.IGNORED_CATEGORIES)
                 .get_descendants(include_self=True)
             )
 
-            return Category.objects.exclude(id__in=categories_to_exclude)
+            result_categories = Category.objects.exclude(id__in=categories_to_exclude)
 
-        def prepare_products(categories_):
+            if utm == 'YM':
+                """
+                Yandex Market feed requires items in some categories to have pictures
+                To simplify filtering we are excluding all categories
+                which don't contain at least one product with picture
+                """
+                result_categories = result_categories.get_categories_tree_with_pictures()
+
+            return result_categories
+
+        def prepare_products(categories_, utm):
             """Filter product list and patch it for rendering."""
             products_except_others = (
                 Product.objects
                 .select_related('page')
                 .prefetch_related('category')
+                .prefetch_related('page__images')
                 .filter(category__in=categories_, price__gt=0)
             )
 
+            if utm == 'YM':
+                """
+                Yandex Market feed requires items in some categories to have pictures
+                To simplify filtering we are excluding all products without pictures
+                """
+                products_except_others = (
+                    products_except_others
+                    .filter(page__images__isnull=False)
+                    .distinct()
+                )
+
             result_products = [
                 put_crumbs(put_utm(product))
                 for product in products_except_others
@@ -93,11 +115,11 @@ def prepare_products(categories_):
             return result_products
 
         categories = (
-            filter_categories() if utm != 'SE78'
+            filter_categories(utm) if utm != 'SE78'
             else Category.objects.all()
         )
 
-        products = prepare_products(categories)
+        products = prepare_products(categories, utm)
 
         return {
             'base_url': settings.BASE_URL,
diff --git a/shopelectro/models.py b/shopelectro/models.py
--- a/shopelectro/models.py
+++ b/shopelectro/models.py
@@ -8,17 +8,36 @@
 from django.urls import reverse
 from django.utils.text import slugify
 from django.utils.translation import ugettext_lazy as _
+from mptt.querysets import TreeQuerySet
 from unidecode import unidecode
 
 from catalog.models import (
-    AbstractProduct, AbstractCategory
+    AbstractCategory,
+    AbstractProduct,
+    CategoryManager,
 )
 from ecommerce.models import Order as ecOrder
 from pages.models import CustomPage, ModelPage, SyncPageMixin
 
 
+class SECategoryQuerySet(TreeQuerySet):
+    def get_categories_tree_with_pictures(self) -> 'SECategoryQuerySet':
+        categories_with_pictures = (
+            self
+            .filter(products__page__images__isnull=False)
+            .distinct()
+        )
+
+        return categories_with_pictures.get_ancestors(include_self=True)
+
+
+class SECategoryManager(CategoryManager.from_queryset(SECategoryQuerySet)):
+    pass
+
+
 class Category(AbstractCategory, SyncPageMixin):
 
+    objects = SECategoryManager()
     uuid = models.UUIDField(default=uuid4, editable=False)
 
     @classmethod


Test patch:
diff --git a/shopelectro/tests/tests_commands.py b/shopelectro/tests/tests_commands.py
--- a/shopelectro/tests/tests_commands.py
+++ b/shopelectro/tests/tests_commands.py
@@ -146,12 +146,24 @@ def get_price_file_path(filename):
         return os.path.join(settings.ASSETS_DIR, filename)
 
     @classmethod
-    def get_price_xml_node(cls):
-        return ElementTree.parse(cls.get_price_file_path('priceru.xml'))
+    def get_price_node(cls, filename):
+        return ElementTree.parse(cls.get_price_file_path(filename))
+
+    @classmethod
+    def get_price_shop_node(cls, filename):
+        return cls.get_price_node(filename).getroot().find('shop')
+
+    @classmethod
+    def get_price_categories_node(cls, filename):
+        return cls.get_price_shop_node(filename).find('categories')
+
+    @classmethod
+    def get_price_offers_node(cls, filename):
+        return cls.get_price_shop_node(filename).find('offers')
 
     def test_prices_exists(self):
         """Price command should generate various price-list files."""
-        price_file_min_size = 10 ** 4  # ~10kb
+        price_file_min_size = 10 ** 3  # ~1kb
 
         for name in price.Command.TARGETS.values():
             file_name = self.get_price_file_path(name)
@@ -160,9 +172,23 @@ def test_prices_exists(self):
             self.assertGreaterEqual(size, price_file_min_size)
 
     def test_categories_in_price(self):
-        categories_in_price = self.get_price_xml_node().getroot().find('shop').find('categories')
+        categories_in_price = self.get_price_categories_node('priceru.xml')
         self.assertEqual(len(categories_in_price), Category.objects.count())
 
+    def test_categories_in_yandex_price(self):
+        categories_in_yandex_price = self.get_price_categories_node('yandex.yml')
+        self.assertEqual(
+            len(categories_in_yandex_price),
+            Category.objects.get_categories_tree_with_pictures().count()
+        )
+
     def test_products_in_price(self):
-        products_in_price = self.get_price_xml_node().getroot().find('shop').find('offers')
+        products_in_price = self.get_price_offers_node('priceru.xml')
         self.assertEqual(len(products_in_price), Product.objects.count())
+
+    def test_products_in_yandex_price(self):
+        products_in_yandex_price = self.get_price_offers_node('yandex.yml')
+        self.assertEqual(
+            len(products_in_yandex_price),
+            Product.objects.filter(page__images__isnull=False).distinct().count()
+        )