We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
ProductPage and ya.market prices not equals
This product in `yandex.yml` (from prod) has price `200 RUB` on it's product page

```
        <offer id="4928" available="true">
          <url>https://www.shopelectro.ru/catalog/products/4928/?utm_source=YM&amp;utm_medium=cpc&amp;utm_content=akkumuliatory-270&amp;utm_term=4928</url>
          <price>199.98</price>
          <purchase_price>106.8111</purchase_price>
          <currencyId>RUR</currencyId>
```

--- 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/management/commands/_update_catalog/update_products.py`
Content:
```
1 import logging
2 from collections import defaultdict
3 from copy import deepcopy
4 from functools import reduce
5 from itertools import chain
6 from typing import Dict, Iterator, List
7 from xml.etree.ElementTree import Element
8 
9 from django.conf import settings
10 from django.contrib.auth.models import User
11 from django.core.mail import send_mail
12 from django.db import transaction
13 from django.db.models import QuerySet
14 from django.template.loader import render_to_string
15 
16 from shopelectro.management.commands._update_catalog.utils import (
17     XmlFile, is_correct_uuid, NOT_SAVE_TEMPLATE, UUID, Data,
18 )
19 from shopelectro.models import Product, ProductPage, Tag
20 
21 
22 logger = logging.getLogger(__name__)
23 
24 
25 def fetch_products(root: Element, config: XmlFile) -> Iterator:
26     product_els = root.findall(config.xpaths['products'])
27     for product_el in product_els:
28         name = product_el.find(config.xpaths['name']).text
29         uuid = product_el.find(config.xpaths['uuid']).text
30         vendor_code = product_el.find(
31             config.xpaths['vendor_code']
32         ).text.lstrip('0')
33         content = product_el.find(config.xpaths['page_content']).text or ''
34 
35         tag_value_els = (
36             tag_el.find(config.xpaths['tag_value_uuid'])
37             for tag_el in product_el.findall(config.xpaths['tags'])
38             if tag_el is not None
39         )
40 
41         tag_uuids = list(filter(is_correct_uuid, (
42             tag_value.text
43             for tag_value in tag_value_els
44             # should use 'is not None', because __bool__ does not defined
45             if tag_value is not None
46         )))
47 
48         tags = Tag.objects.filter(uuid__in=tag_uuids)
49 
50         yield uuid, {
51             'name': name,
52             'vendor_code': vendor_code,
53             'page': {
54                 'content': content
55             },
56             'tags': tags
57         }
58 
59 
60 def fetch_prices(root: Element, config) -> Iterator:
61     def get_prices(prices_el):
62         def get_(price_el: Element) -> float:
63             return float(price_el.find(config.xpaths['price']).text)
64         *prices, retail_price = sorted(
65             get_(price)
66             for price in prices_el.findall(config.xpaths['prices'])
67         )
68         return prices + [retail_price - settings.PRICE_REDUCER]
69     product_price_els = root.findall(config.xpaths['product_prices'])
70     for prices_el in product_price_els:
71         product_uuid = prices_el.find(config.xpaths['product_uuid']).text
72         prices = dict(zip(
73             config.extra_options['price_types'],
74             get_prices(prices_el)
75         ))
76         yield product_uuid, prices
77 
78 
79 def fetch_in_stock(root: Element, config: XmlFile) -> Iterator:
80     product_els = root.findall(config.xpaths['products'])
81     for product_el in product_els:
82         uuid = product_el.find(config.xpaths['product_uuid']).text
83         in_stock = product_el.find(config.xpaths['in_stock']).text
84 
85         if not (in_stock.isdigit() and int(in_stock) >= 0):
86             in_stock = 0
87 
88         yield uuid, {
89             'in_stock': in_stock,
90         }
91 
92 
93 product_file = XmlFile(
94     fetch_callback=fetch_products,
95     xml_path_pattern='**/webdata/**/goods/**/import*.xml',
96     xpath_queries={
97         'products': './/{}Товары/',
98         'name': '.{}Наименование',
99         'uuid': '.{}Ид',
100         'page_content': '.{}Описание',
101         'tags': '.{}ЗначенияСвойств/',
102         'tag_value_uuid': '.{}Значение',
103         'vendor_code': '.{0}ЗначенияРеквизитов/{0}ЗначениеРеквизита'
104                        '[{0}Наименование="Код"]/{0}Значение',
105     },
106 )
107 
108 price_file = XmlFile(
109     fetch_callback=fetch_prices,
110     xml_path_pattern='**/webdata/**/goods/**/prices*.xml',
111     xpath_queries={
112         'product_prices': './/{}Предложения/',
113         'product_uuid': '.{}Ид',
114         'prices': '.{}Цены/',
115         'price': '.{}ЦенаЗаЕдиницу',
116     },
117     extra_options={
118         'price_types': [
119             'purchase_price', 'wholesale_large', 'wholesale_medium',
120             'wholesale_small', 'price',
121         ],
122     },
123 )
124 
125 
126 in_stock_file = XmlFile(
127     fetch_callback=fetch_in_stock,
128     xml_path_pattern='**/webdata/**/goods/**/rests*.xml',
129     xpath_queries={
130         'products': './/{}Предложения/',
131         'product_uuid': '.{}Ид',
132         'in_stock': './/{}Количество',
133     },
134 )
135 
136 
137 def merge_data(*data) -> Dict[UUID, Data]:
138     """
139     Merge data from xml files with different structure.
140 
141     Example: files with product names and prices.
142     """
143     product_data = defaultdict(dict)
144     for key, data in chain.from_iterable(filter(None, data)):
145         product_data[key].update(data)
146 
147     return product_data
148 
149 
150 def clean_data(data: Dict[UUID, Data]):
151     def has_all_prices(_, product_data):
152         price_types = price_file.extra_options['price_types']
153         has = all(
154             product_data.get(price_type)
155             for price_type in price_types
156         )
157         if not has:
158             logger.info(NOT_SAVE_TEMPLATE.format(
159                 entity='Product',
160                 name=product_data['name'],
161                 field='price'
162             ))
163         return has
164 
165     def has_vendor_code(_, product_data):
166         has = bool(product_data['vendor_code'])
167 
168         if not has:
169             logger.info(NOT_SAVE_TEMPLATE.format(
170                 entity='Product',
171                 name=product_data['name'],
172                 field='vendor_code'
173             ))
174 
175         return has
176 
177     def has_uuid(uuid, product_data):
178         has = is_correct_uuid(uuid)
179         if not has:
180             logger.info(NOT_SAVE_TEMPLATE.format(
181                 entity='Product',
182                 name=product_data['name'],
183                 field='uuid'
184             ))
185         return has
186 
187     def filter_(product_data):
188         return all(
189             f(*product_data)
190             for f in [has_all_prices, has_uuid, has_vendor_code]
191         )
192 
193     cleaned_data = dict(
194         product_data
195         for product_data in data.items()
196         if filter_(product_data)
197     )
198 
199     return cleaned_data
200 
201 
202 def report(recipients=None, message=None):
203     message = message or render_to_string('report.html')
204 
205     user_query = (
206         User.objects
207             .filter(is_staff=True, is_superuser=False, is_active=True, email__isnull=False)
208     )
209 
210     recipient_list = recipients or [user.email for user in user_query]
211 
212     if recipient_list:
213         send_mail(
214             subject='Обновления каталога товаров',
215             message=message,
216             from_email=settings.EMAIL_SENDER,
217             recipient_list=recipient_list,
218             html_message=message,
219         )
220 
221         logger.info('Sent message to {}'.format(
222             reduce(lambda x, y: '{}, {}'.format(x, y), recipient_list)
223         ))
224 
225 
226 @transaction.atomic
227 def delete(data: Dict[UUID, Data]):
228     uuids = list(data)
229     page_count, _ = ProductPage.objects.exclude(
230         shopelectro_product__uuid__in=uuids).delete()
231     product_count, _ = Product.objects.exclude(
232         uuid__in=uuids).delete()
233     logger.info('{} products and {} pages were deleted.'.format(
234         product_count, page_count))
235 
236 
237 @transaction.atomic
238 def update(data: Dict[UUID, Data]) -> QuerySet:
239     def save(product, field, value):
240         if field == 'name' and getattr(product, field, None):
241             return
242         elif field == 'page':
243             for page_field, page_value in value.items():
244                 if not getattr(product.page, page_field, ''):
245                     setattr(product.page, page_field, page_value)
246         else:
247             setattr(product, field, value)
248 
249     def merge(left: List, right: List) -> List:
250         """Merge two arrays with order preserving."""
251         return left + [e for e in right if e not in left]
252 
253     products = Product.objects.filter(uuid__in=data)
254 
255     for product in products:
256         product_data = data[str(product.uuid)]
257         for field, value in product_data.items():
258             if field != 'tags':
259                 save(product, field, value)
260             else:
261                 # Dirty patch for preserving tags, appended from admin.
262                 # Still waiting 1C throwing out.
263                 product.tags = merge(list(product.tags.all()), value)
264 
265         product.save()
266     logger.info('{} products were updated.'.format(products.count()))
267     return products
268 
269 
270 @transaction.atomic
271 def create(data: Dict[UUID, Data], updated_products: QuerySet) -> QuerySet:
272     data = deepcopy(data)
273     uuids_for_create = (
274         set(data) - set(str(product.uuid) for product in updated_products)
275     )
276 
277     for uuid in uuids_for_create:
278         product_data = data.get(uuid)
279         tags = product_data.pop('tags', {})
280         page_data = product_data.pop('page', {})
281 
282         new_product = Product.objects.create(**product_data, uuid=uuid)
283         new_product.tags.set(tags)
284         for field, value in page_data.items():
285             setattr(new_product.page, field, value)
286         new_product.page.save()
287 
288     created_products = Product.objects.filter(uuid__in=uuids_for_create)
289 
290     logger.info('{} products were created.'.format(created_products.count()))
291     return created_products
292 
293 
294 class UpdateProductError(Exception):
295     pass
296 
297 
298 def main(*args, **kwargs):
299     cleaned_product_data = clean_data(merge_data(
300         product_file.get_data(),
301         price_file.get_data(),
302         in_stock_file.get_data(),
303     ))
304 
305     if not cleaned_product_data:
306 
307         parsed_files = {
308             'product_files': list(product_file.parsed_files),
309             'price_files': list(price_file.parsed_files),
310             'in_stock_files': list(in_stock_file.parsed_files),
311         }
312 
313         if not any(parsed_files.values()):
314             message = 'Files does not exist: {}'.format(parsed_files)
315         else:
316             # TODO: happy debugging (:
317             message = 'The file structure has changed or it does not contain the required data.'
318 
319         raise UpdateProductError(message)
320 
321     delete(cleaned_product_data)
322     updated_products = update(cleaned_product_data)
323     created_products = create(cleaned_product_data, updated_products)
324 
325     if created_products.exists():
326         report(kwargs['recipients'])
327
```
--- 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_products.py b/shopelectro/management/commands/_update_catalog/update_products.py
--- a/shopelectro/management/commands/_update_catalog/update_products.py
+++ b/shopelectro/management/commands/_update_catalog/update_products.py
@@ -61,11 +61,13 @@
     def get_prices(prices_el):
         def get_(price_el: Element) -> float:
             return float(price_el.find(config.xpaths['price']).text)
+        def prepare_retail_(price: float):
+            return round(price - settings.PRICE_REDUCER)
         *prices, retail_price = sorted(
             get_(price)
             for price in prices_el.findall(config.xpaths['prices'])
         )
-        return prices + [retail_price - settings.PRICE_REDUCER]
+        return prices + [prepare_retail_(retail_price)]
     product_price_els = root.findall(config.xpaths['product_prices'])
     for prices_el in product_price_els:
         product_uuid = prices_el.find(config.xpaths['product_uuid']).text

{"golden_diff": "diff --git a/shopelectro/management/commands/_update_catalog/update_products.py b/shopelectro/management/commands/_update_catalog/update_products.py\n--- a/shopelectro/management/commands/_update_catalog/update_products.py\n+++ b/shopelectro/management/commands/_update_catalog/update_products.py\n@@ -61,11 +61,13 @@\n     def get_prices(prices_el):\n         def get_(price_el: Element) -> float:\n             return float(price_el.find(config.xpaths['price']).text)\n+        def prepare_retail_(price: float):\n+            return round(price - settings.PRICE_REDUCER)\n         *prices, retail_price = sorted(\n             get_(price)\n             for price in prices_el.findall(config.xpaths['prices'])\n         )\n-        return prices + [retail_price - settings.PRICE_REDUCER]\n+        return prices + [prepare_retail_(retail_price)]\n     product_price_els = root.findall(config.xpaths['product_prices'])\n     for prices_el in product_price_els:\n         product_uuid = prices_el.find(config.xpaths['product_uuid']).text\n", "issue": "ProductPage and ya.market prices not equals\nThis product in `yandex.yml` (from prod) has price `200 RUB` on it's product page\r\n\r\n```\r\n        <offer id=\"4928\" available=\"true\">\r\n          <url>https://www.shopelectro.ru/catalog/products/4928/?utm_source=YM&amp;utm_medium=cpc&amp;utm_content=akkumuliatory-270&amp;utm_term=4928</url>\r\n          <price>199.98</price>\r\n          <purchase_price>106.8111</purchase_price>\r\n          <currencyId>RUR</currencyId>\r\n```\n", "before_files": [{"content": "import logging\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom functools import reduce\nfrom itertools import chain\nfrom typing import Dict, Iterator, List\nfrom xml.etree.ElementTree import Element\n\nfrom django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.core.mail import send_mail\nfrom django.db import transaction\nfrom django.db.models import QuerySet\nfrom django.template.loader import render_to_string\n\nfrom shopelectro.management.commands._update_catalog.utils import (\n    XmlFile, is_correct_uuid, NOT_SAVE_TEMPLATE, UUID, Data,\n)\nfrom shopelectro.models import Product, ProductPage, Tag\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef fetch_products(root: Element, config: XmlFile) -> Iterator:\n    product_els = root.findall(config.xpaths['products'])\n    for product_el in product_els:\n        name = product_el.find(config.xpaths['name']).text\n        uuid = product_el.find(config.xpaths['uuid']).text\n        vendor_code = product_el.find(\n            config.xpaths['vendor_code']\n        ).text.lstrip('0')\n        content = product_el.find(config.xpaths['page_content']).text or ''\n\n        tag_value_els = (\n            tag_el.find(config.xpaths['tag_value_uuid'])\n            for tag_el in product_el.findall(config.xpaths['tags'])\n            if tag_el is not None\n        )\n\n        tag_uuids = list(filter(is_correct_uuid, (\n            tag_value.text\n            for tag_value in tag_value_els\n            # should use 'is not None', because __bool__ does not defined\n            if tag_value is not None\n        )))\n\n        tags = Tag.objects.filter(uuid__in=tag_uuids)\n\n        yield uuid, {\n            'name': name,\n            'vendor_code': vendor_code,\n            'page': {\n                'content': content\n            },\n            'tags': tags\n        }\n\n\ndef fetch_prices(root: Element, config) -> Iterator:\n    def get_prices(prices_el):\n        def get_(price_el: Element) -> float:\n            return float(price_el.find(config.xpaths['price']).text)\n        *prices, retail_price = sorted(\n            get_(price)\n            for price in prices_el.findall(config.xpaths['prices'])\n        )\n        return prices + [retail_price - settings.PRICE_REDUCER]\n    product_price_els = root.findall(config.xpaths['product_prices'])\n    for prices_el in product_price_els:\n        product_uuid = prices_el.find(config.xpaths['product_uuid']).text\n        prices = dict(zip(\n            config.extra_options['price_types'],\n            get_prices(prices_el)\n        ))\n        yield product_uuid, prices\n\n\ndef fetch_in_stock(root: Element, config: XmlFile) -> Iterator:\n    product_els = root.findall(config.xpaths['products'])\n    for product_el in product_els:\n        uuid = product_el.find(config.xpaths['product_uuid']).text\n        in_stock = product_el.find(config.xpaths['in_stock']).text\n\n        if not (in_stock.isdigit() and int(in_stock) >= 0):\n            in_stock = 0\n\n        yield uuid, {\n            'in_stock': in_stock,\n        }\n\n\nproduct_file = XmlFile(\n    fetch_callback=fetch_products,\n    xml_path_pattern='**/webdata/**/goods/**/import*.xml',\n    xpath_queries={\n        'products': './/{}\u0422\u043e\u0432\u0430\u0440\u044b/',\n        'name': '.{}\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435',\n        'uuid': '.{}\u0418\u0434',\n        'page_content': '.{}\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435',\n        'tags': '.{}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0421\u0432\u043e\u0439\u0441\u0442\u0432/',\n        'tag_value_uuid': '.{}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435',\n        'vendor_code': '.{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432/{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430'\n                       '[{0}\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435=\"\u041a\u043e\u0434\"]/{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435',\n    },\n)\n\nprice_file = XmlFile(\n    fetch_callback=fetch_prices,\n    xml_path_pattern='**/webdata/**/goods/**/prices*.xml',\n    xpath_queries={\n        'product_prices': './/{}\u041f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f/',\n        'product_uuid': '.{}\u0418\u0434',\n        'prices': '.{}\u0426\u0435\u043d\u044b/',\n        'price': '.{}\u0426\u0435\u043d\u0430\u0417\u0430\u0415\u0434\u0438\u043d\u0438\u0446\u0443',\n    },\n    extra_options={\n        'price_types': [\n            'purchase_price', 'wholesale_large', 'wholesale_medium',\n            'wholesale_small', 'price',\n        ],\n    },\n)\n\n\nin_stock_file = XmlFile(\n    fetch_callback=fetch_in_stock,\n    xml_path_pattern='**/webdata/**/goods/**/rests*.xml',\n    xpath_queries={\n        'products': './/{}\u041f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f/',\n        'product_uuid': '.{}\u0418\u0434',\n        'in_stock': './/{}\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e',\n    },\n)\n\n\ndef merge_data(*data) -> Dict[UUID, Data]:\n    \"\"\"\n    Merge data from xml files with different structure.\n\n    Example: files with product names and prices.\n    \"\"\"\n    product_data = defaultdict(dict)\n    for key, data in chain.from_iterable(filter(None, data)):\n        product_data[key].update(data)\n\n    return product_data\n\n\ndef clean_data(data: Dict[UUID, Data]):\n    def has_all_prices(_, product_data):\n        price_types = price_file.extra_options['price_types']\n        has = all(\n            product_data.get(price_type)\n            for price_type in price_types\n        )\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='price'\n            ))\n        return has\n\n    def has_vendor_code(_, product_data):\n        has = bool(product_data['vendor_code'])\n\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='vendor_code'\n            ))\n\n        return has\n\n    def has_uuid(uuid, product_data):\n        has = is_correct_uuid(uuid)\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='uuid'\n            ))\n        return has\n\n    def filter_(product_data):\n        return all(\n            f(*product_data)\n            for f in [has_all_prices, has_uuid, has_vendor_code]\n        )\n\n    cleaned_data = dict(\n        product_data\n        for product_data in data.items()\n        if filter_(product_data)\n    )\n\n    return cleaned_data\n\n\ndef report(recipients=None, message=None):\n    message = message or render_to_string('report.html')\n\n    user_query = (\n        User.objects\n            .filter(is_staff=True, is_superuser=False, is_active=True, email__isnull=False)\n    )\n\n    recipient_list = recipients or [user.email for user in user_query]\n\n    if recipient_list:\n        send_mail(\n            subject='\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432',\n            message=message,\n            from_email=settings.EMAIL_SENDER,\n            recipient_list=recipient_list,\n            html_message=message,\n        )\n\n        logger.info('Sent message to {}'.format(\n            reduce(lambda x, y: '{}, {}'.format(x, y), recipient_list)\n        ))\n\n\n@transaction.atomic\ndef delete(data: Dict[UUID, Data]):\n    uuids = list(data)\n    page_count, _ = ProductPage.objects.exclude(\n        shopelectro_product__uuid__in=uuids).delete()\n    product_count, _ = Product.objects.exclude(\n        uuid__in=uuids).delete()\n    logger.info('{} products and {} pages were deleted.'.format(\n        product_count, page_count))\n\n\n@transaction.atomic\ndef update(data: Dict[UUID, Data]) -> QuerySet:\n    def save(product, field, value):\n        if field == 'name' and getattr(product, field, None):\n            return\n        elif field == 'page':\n            for page_field, page_value in value.items():\n                if not getattr(product.page, page_field, ''):\n                    setattr(product.page, page_field, page_value)\n        else:\n            setattr(product, field, value)\n\n    def merge(left: List, right: List) -> List:\n        \"\"\"Merge two arrays with order preserving.\"\"\"\n        return left + [e for e in right if e not in left]\n\n    products = Product.objects.filter(uuid__in=data)\n\n    for product in products:\n        product_data = data[str(product.uuid)]\n        for field, value in product_data.items():\n            if field != 'tags':\n                save(product, field, value)\n            else:\n                # Dirty patch for preserving tags, appended from admin.\n                # Still waiting 1C throwing out.\n                product.tags = merge(list(product.tags.all()), value)\n\n        product.save()\n    logger.info('{} products were updated.'.format(products.count()))\n    return products\n\n\n@transaction.atomic\ndef create(data: Dict[UUID, Data], updated_products: QuerySet) -> QuerySet:\n    data = deepcopy(data)\n    uuids_for_create = (\n        set(data) - set(str(product.uuid) for product in updated_products)\n    )\n\n    for uuid in uuids_for_create:\n        product_data = data.get(uuid)\n        tags = product_data.pop('tags', {})\n        page_data = product_data.pop('page', {})\n\n        new_product = Product.objects.create(**product_data, uuid=uuid)\n        new_product.tags.set(tags)\n        for field, value in page_data.items():\n            setattr(new_product.page, field, value)\n        new_product.page.save()\n\n    created_products = Product.objects.filter(uuid__in=uuids_for_create)\n\n    logger.info('{} products were created.'.format(created_products.count()))\n    return created_products\n\n\nclass UpdateProductError(Exception):\n    pass\n\n\ndef main(*args, **kwargs):\n    cleaned_product_data = clean_data(merge_data(\n        product_file.get_data(),\n        price_file.get_data(),\n        in_stock_file.get_data(),\n    ))\n\n    if not cleaned_product_data:\n\n        parsed_files = {\n            'product_files': list(product_file.parsed_files),\n            'price_files': list(price_file.parsed_files),\n            'in_stock_files': list(in_stock_file.parsed_files),\n        }\n\n        if not any(parsed_files.values()):\n            message = 'Files does not exist: {}'.format(parsed_files)\n        else:\n            # TODO: happy debugging (:\n            message = 'The file structure has changed or it does not contain the required data.'\n\n        raise UpdateProductError(message)\n\n    delete(cleaned_product_data)\n    updated_products = update(cleaned_product_data)\n    created_products = create(cleaned_product_data, updated_products)\n\n    if created_products.exists():\n        report(kwargs['recipients'])\n", "path": "shopelectro/management/commands/_update_catalog/update_products.py"}], "after_files": [{"content": "import logging\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom functools import reduce\nfrom itertools import chain\nfrom typing import Dict, Iterator, List\nfrom xml.etree.ElementTree import Element\n\nfrom django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.core.mail import send_mail\nfrom django.db import transaction\nfrom django.db.models import QuerySet\nfrom django.template.loader import render_to_string\n\nfrom shopelectro.management.commands._update_catalog.utils import (\n    XmlFile, is_correct_uuid, NOT_SAVE_TEMPLATE, UUID, Data,\n)\nfrom shopelectro.models import Product, ProductPage, Tag\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef fetch_products(root: Element, config: XmlFile) -> Iterator:\n    product_els = root.findall(config.xpaths['products'])\n    for product_el in product_els:\n        name = product_el.find(config.xpaths['name']).text\n        uuid = product_el.find(config.xpaths['uuid']).text\n        vendor_code = product_el.find(\n            config.xpaths['vendor_code']\n        ).text.lstrip('0')\n        content = product_el.find(config.xpaths['page_content']).text or ''\n\n        tag_value_els = (\n            tag_el.find(config.xpaths['tag_value_uuid'])\n            for tag_el in product_el.findall(config.xpaths['tags'])\n            if tag_el is not None\n        )\n\n        tag_uuids = list(filter(is_correct_uuid, (\n            tag_value.text\n            for tag_value in tag_value_els\n            # should use 'is not None', because __bool__ does not defined\n            if tag_value is not None\n        )))\n\n        tags = Tag.objects.filter(uuid__in=tag_uuids)\n\n        yield uuid, {\n            'name': name,\n            'vendor_code': vendor_code,\n            'page': {\n                'content': content\n            },\n            'tags': tags\n        }\n\n\ndef fetch_prices(root: Element, config) -> Iterator:\n    def get_prices(prices_el):\n        def get_(price_el: Element) -> float:\n            return float(price_el.find(config.xpaths['price']).text)\n        def prepare_retail_(price: float):\n            return round(price - settings.PRICE_REDUCER)\n        *prices, retail_price = sorted(\n            get_(price)\n            for price in prices_el.findall(config.xpaths['prices'])\n        )\n        return prices + [prepare_retail_(retail_price)]\n    product_price_els = root.findall(config.xpaths['product_prices'])\n    for prices_el in product_price_els:\n        product_uuid = prices_el.find(config.xpaths['product_uuid']).text\n        prices = dict(zip(\n            config.extra_options['price_types'],\n            get_prices(prices_el)\n        ))\n        yield product_uuid, prices\n\n\ndef fetch_in_stock(root: Element, config: XmlFile) -> Iterator:\n    product_els = root.findall(config.xpaths['products'])\n    for product_el in product_els:\n        uuid = product_el.find(config.xpaths['product_uuid']).text\n        in_stock = product_el.find(config.xpaths['in_stock']).text\n\n        if not (in_stock.isdigit() and int(in_stock) >= 0):\n            in_stock = 0\n\n        yield uuid, {\n            'in_stock': in_stock,\n        }\n\n\nproduct_file = XmlFile(\n    fetch_callback=fetch_products,\n    xml_path_pattern='**/webdata/**/goods/**/import*.xml',\n    xpath_queries={\n        'products': './/{}\u0422\u043e\u0432\u0430\u0440\u044b/',\n        'name': '.{}\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435',\n        'uuid': '.{}\u0418\u0434',\n        'page_content': '.{}\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435',\n        'tags': '.{}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0421\u0432\u043e\u0439\u0441\u0442\u0432/',\n        'tag_value_uuid': '.{}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435',\n        'vendor_code': '.{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u043e\u0432/{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442\u0430'\n                       '[{0}\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435=\"\u041a\u043e\u0434\"]/{0}\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435',\n    },\n)\n\nprice_file = XmlFile(\n    fetch_callback=fetch_prices,\n    xml_path_pattern='**/webdata/**/goods/**/prices*.xml',\n    xpath_queries={\n        'product_prices': './/{}\u041f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f/',\n        'product_uuid': '.{}\u0418\u0434',\n        'prices': '.{}\u0426\u0435\u043d\u044b/',\n        'price': '.{}\u0426\u0435\u043d\u0430\u0417\u0430\u0415\u0434\u0438\u043d\u0438\u0446\u0443',\n    },\n    extra_options={\n        'price_types': [\n            'purchase_price', 'wholesale_large', 'wholesale_medium',\n            'wholesale_small', 'price',\n        ],\n    },\n)\n\n\nin_stock_file = XmlFile(\n    fetch_callback=fetch_in_stock,\n    xml_path_pattern='**/webdata/**/goods/**/rests*.xml',\n    xpath_queries={\n        'products': './/{}\u041f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f/',\n        'product_uuid': '.{}\u0418\u0434',\n        'in_stock': './/{}\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e',\n    },\n)\n\n\ndef merge_data(*data) -> Dict[UUID, Data]:\n    \"\"\"\n    Merge data from xml files with different structure.\n\n    Example: files with product names and prices.\n    \"\"\"\n    product_data = defaultdict(dict)\n    for key, data in chain.from_iterable(filter(None, data)):\n        product_data[key].update(data)\n\n    return product_data\n\n\ndef clean_data(data: Dict[UUID, Data]):\n    def has_all_prices(_, product_data):\n        price_types = price_file.extra_options['price_types']\n        has = all(\n            product_data.get(price_type)\n            for price_type in price_types\n        )\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='price'\n            ))\n        return has\n\n    def has_vendor_code(_, product_data):\n        has = bool(product_data['vendor_code'])\n\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='vendor_code'\n            ))\n\n        return has\n\n    def has_uuid(uuid, product_data):\n        has = is_correct_uuid(uuid)\n        if not has:\n            logger.info(NOT_SAVE_TEMPLATE.format(\n                entity='Product',\n                name=product_data['name'],\n                field='uuid'\n            ))\n        return has\n\n    def filter_(product_data):\n        return all(\n            f(*product_data)\n            for f in [has_all_prices, has_uuid, has_vendor_code]\n        )\n\n    cleaned_data = dict(\n        product_data\n        for product_data in data.items()\n        if filter_(product_data)\n    )\n\n    return cleaned_data\n\n\ndef report(recipients=None, message=None):\n    message = message or render_to_string('report.html')\n\n    user_query = (\n        User.objects\n            .filter(is_staff=True, is_superuser=False, is_active=True, email__isnull=False)\n    )\n\n    recipient_list = recipients or [user.email for user in user_query]\n\n    if recipient_list:\n        send_mail(\n            subject='\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432',\n            message=message,\n            from_email=settings.EMAIL_SENDER,\n            recipient_list=recipient_list,\n            html_message=message,\n        )\n\n        logger.info('Sent message to {}'.format(\n            reduce(lambda x, y: '{}, {}'.format(x, y), recipient_list)\n        ))\n\n\n@transaction.atomic\ndef delete(data: Dict[UUID, Data]):\n    uuids = list(data)\n    page_count, _ = ProductPage.objects.exclude(\n        shopelectro_product__uuid__in=uuids).delete()\n    product_count, _ = Product.objects.exclude(\n        uuid__in=uuids).delete()\n    logger.info('{} products and {} pages were deleted.'.format(\n        product_count, page_count))\n\n\n@transaction.atomic\ndef update(data: Dict[UUID, Data]) -> QuerySet:\n    def save(product, field, value):\n        if field == 'name' and getattr(product, field, None):\n            return\n        elif field == 'page':\n            for page_field, page_value in value.items():\n                if not getattr(product.page, page_field, ''):\n                    setattr(product.page, page_field, page_value)\n        else:\n            setattr(product, field, value)\n\n    def merge(left: List, right: List) -> List:\n        \"\"\"Merge two arrays with order preserving.\"\"\"\n        return left + [e for e in right if e not in left]\n\n    products = Product.objects.filter(uuid__in=data)\n\n    for product in products:\n        product_data = data[str(product.uuid)]\n        for field, value in product_data.items():\n            if field != 'tags':\n                save(product, field, value)\n            else:\n                # Dirty patch for preserving tags, appended from admin.\n                # Still waiting 1C throwing out.\n                product.tags = merge(list(product.tags.all()), value)\n\n        product.save()\n    logger.info('{} products were updated.'.format(products.count()))\n    return products\n\n\n@transaction.atomic\ndef create(data: Dict[UUID, Data], updated_products: QuerySet) -> QuerySet:\n    data = deepcopy(data)\n    uuids_for_create = (\n        set(data) - set(str(product.uuid) for product in updated_products)\n    )\n\n    for uuid in uuids_for_create:\n        product_data = data.get(uuid)\n        tags = product_data.pop('tags', {})\n        page_data = product_data.pop('page', {})\n\n        new_product = Product.objects.create(**product_data, uuid=uuid)\n        new_product.tags.set(tags)\n        for field, value in page_data.items():\n            setattr(new_product.page, field, value)\n        new_product.page.save()\n\n    created_products = Product.objects.filter(uuid__in=uuids_for_create)\n\n    logger.info('{} products were created.'.format(created_products.count()))\n    return created_products\n\n\nclass UpdateProductError(Exception):\n    pass\n\n\ndef main(*args, **kwargs):\n    cleaned_product_data = clean_data(merge_data(\n        product_file.get_data(),\n        price_file.get_data(),\n        in_stock_file.get_data(),\n    ))\n\n    if not cleaned_product_data:\n\n        parsed_files = {\n            'product_files': list(product_file.parsed_files),\n            'price_files': list(price_file.parsed_files),\n            'in_stock_files': list(in_stock_file.parsed_files),\n        }\n\n        if not any(parsed_files.values()):\n            message = 'Files does not exist: {}'.format(parsed_files)\n        else:\n            # TODO: happy debugging (:\n            message = 'The file structure has changed or it does not contain the required data.'\n\n        raise UpdateProductError(message)\n\n    delete(cleaned_product_data)\n    updated_products = update(cleaned_product_data)\n    created_products = create(cleaned_product_data, updated_products)\n\n    if created_products.exists():\n        report(kwargs['recipients'])\n", "path": "shopelectro/management/commands/_update_catalog/update_products.py"}]}