Django mptt error

Question:

There are product categories and products themselves. Models.py file:

class Category(MPTTModel):
    name = models.CharField(default='',
                            max_length=50,
                            verbose_name='Название')
    slug = models.SlugField(default='')
    parent = TreeForeignKey('self',
                            related_name='children',
                            null=True,
                            blank=True,
                            verbose_name='Родительская категория'
                            )
    order = models.PositiveSmallIntegerField(blank=False,
                                             null=False,
                                             default=0,
                                             verbose_name='Порядок')
    is_active = models.BooleanField(default=True,
                                    db_index=True,
                                    verbose_name='Отображать на сайте')
    sizes_parameters = JSONField(blank=True, null=True, default="[]",
                                 verbose_name='Параметры размеров товара',
                                 help_text='Список параметров, вида: '
                                           '["Размер", "Объем груди", "Рост"]')

    class Meta:
        verbose_name = 'Категория'
        verbose_name_plural = 'категории'

    class MPTTMeta:
        order_insertion_by = ['order']

    def save(self, *args, **kwargs):
        super(Category, self).save(*args, **kwargs)
        Category.objects.rebuild()

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        # return '/shop/catalog/{}/'.format('/'.join([c.slug for c in self.get_family()]))
        return reverse('shop:catalog', args=['{}/'.format('/'.join([c.slug for c in self.get_family()]))])


class Product(MPTTModel):
    id = models.AutoField(primary_key=True)
    name = models.CharField(default='',
                            max_length=50,
                            verbose_name='Название')
    vendor_code = models.CharField(default='', max_length=50,
                                   verbose_name='Артикул')
    slug = models.SlugField(default='')
    price = models.DecimalField(max_digits=7,
                                decimal_places=2,
                                default=0.00,
                                verbose_name='Цена')
    discount = models.PositiveSmallIntegerField(default=0,
                                                db_index=True,
                                                verbose_name='Скидка',
                                                help_text='Указывается в процентах')
    description = models.TextField(blank=True,
                                   null=True,
                                   verbose_name='Описание товара')
    is_active = models.BooleanField(default=True,
                                    db_index=True,
                                    verbose_name='Отображать на сайте')
    parent = TreeForeignKey(Category,
                            related_name='products',
                            blank=True,
                            null=True,
                            verbose_name='Категория')
    image = models.ImageField(upload_to='products/images',
                              blank=True,
                              null=True,
                              verbose_name='Фото товара')
    sizes = JSONField(default=collections.OrderedDict(), verbose_name='Размеры товара',
                      load_kwargs={'object_pairs_hook': collections.OrderedDict})
    created = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
    updated = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')

    class Meta:
        unique_together = ('slug', 'parent')
        verbose_name = 'Товар'
        verbose_name_plural = 'Товары'

    class MPTTMeta:
        order_insertion_by = ['name']

    def save(self, *args, **kwargs):
        super(Product, self).save(*args, **kwargs)
        Product.objects.rebuild()

    def __str__(self):
        return self.name

    def discount_price(self):
        return self.price - (self.price * self.discount / 100)

    def thumbnails_delete(**kwargs):
        thumbnail = get_thumbnailer(kwargs['file'])
        thumbnail.delete_thumbnails()

    cleanup_pre_delete.connect(thumbnails_delete)

I have created several categories with different nesting. When creating a product, the category is empty by default, and if you save a product without specifying a category, then it is saved perfectly, also, if you immediately specify a category for it when creating a product, then it is successfully saved in this category. From time to time, a problem arises if the product does not specify a category, save and then edit the product, indicating to it the FIRST root category, or any of the subcategories of the FIRST root category. I get the error: "mptt.exceptions.InvalidMove: Element cannot be a descendant of its heir." When choosing any other root, or their subcategories, I do not receive such an error. I filled the base with the script:

from shop.models import Product, Category
import os
from django.core.files import File
from collections import OrderedDict

sizes_parameters = ["Размер", "Объем груди", "Объем талии", "Объем бедер", "Длина изделия"]

categories = [
    {'name': 'Женская одежда', 'slug': 'women', 'order': 0, },
    {'name': 'Женская одежда Size+', 'slug': 'women-size', 'order': 1, },
    {'name': 'Детская одежда', 'slug': 'kids', 'order': 2, },
    {'name': 'Аксессуары', 'slug': 'accessories', 'order': 3, },
    {'name': 'Косметика и парфюмерия', 'slug': 'cosmetics', 'order': 4, },
    {'name': 'Обувь', 'slug': 'shoes', 'order': 5, },
    {'name': 'Сумки и кошельки', 'slug': 'bags', 'order': 6, },
    {'name': 'Спальные принадлежности', 'slug': 'sleeping', 'order': 7, },

    {'name': 'Брюки, джинсы, лоссины', 'slug': 'jeans', 'parent': 'women',
     'order': 0, 'sizes_parameters': sizes_parameters},
    {'name': 'Шорты', 'slug': 'shorty', 'parent': 'women', 'order': 1,
     'sizes_parameters': sizes_parameters},
    {'name': 'Комбинезоны', 'slug': 'kombinezony', 'parent': 'women', 'order': 2,
     'sizes_parameters': sizes_parameters},
    {'name': 'Юбки', 'slug': 'yubki', 'parent': 'women', 'order': 3,
     'sizes_parameters': sizes_parameters},
    {'name': 'Свитера, кофты, рубашки', 'slug': 'svitera-kifty-rubashki',
     'parent': 'women', 'order': 4, 'sizes_parameters': sizes_parameters},
    {'name': 'Футболки', 'slug': 'futbolki', 'parent': 'women', 'order': 5,
     'sizes_parameters': sizes_parameters},
    {'name': 'Платья', 'slug': 'platya', 'parent': 'women', 'order': 6,
     'sizes_parameters': sizes_parameters},
    {'name': 'Кардиганы, пиджаки', 'slug': 'kardigany-pidjaki', 'parent': 'women',
     'order': 7, 'sizes_parameters': sizes_parameters},
    {'name': 'Нижнее белье', 'slug': 'nijnee-belye', 'parent': 'women', 'order': 8,
     'sizes_parameters': sizes_parameters},
]

products = [
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza1', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '1.jpg', 'vendor_code': '000001'},
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza2', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '2.jpg', 'vendor_code': '000002'},
    {'name': 'Блуза Креп Коттон', 'slug': 'bluza3', 'price': 2050.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '3.jpg', 'vendor_code': '000003'},
    {'name': 'Стильный бомпер', 'slug': 'bomper4', 'price': 2300.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '4.jpg', 'vendor_code': '000004'},
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza5', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '5.jpg', 'vendor_code': '000005'},
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza6', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '6.jpg', 'vendor_code': '000006'},
    {'name': 'Блуза Креп Коттон', 'slug': 'bluza7', 'price': 2050.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '7.jpg', 'vendor_code': '000007'},
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza8', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '8.jpg', 'vendor_code': '000008'},
    {'name': 'Блуза Креп Шифон', 'slug': 'bluza9', 'price': 1960.00, 'parent': 'women/svitera-kifty-rubashki',
     'sizes': (("Размер", ['42', '44', '46', '48', '50', '52', '54']),
         ("Объем груди",    ['84', '88', '92', '96', '100', '104', '108']),
         ("Объем талии",    ['64', '68', '72', '76', '80', '84', '88']),
         ("Объем бедер", ['90', '94', '98', '102', '106', '110', '114']),
         ("Длина изделия", ['110', '112', '114', '116', '118', '120', '120'])),
     'image': '9.jpg', 'vendor_code': '000009'},
]

print('СОЗДАНИЕ КАТЕГОРИЙ ТОВАРОВ:')
for cat in categories:
    if 'parent' in cat:
        print(cat['name'])
        new_cat = Category.objects.create(name=cat['name'], slug=cat['slug'], order=cat['order'],
                                          parent=Category.objects.get(slug=cat['parent']),
                                          sizes_parameters=cat['sizes_parameters'])
    else:
        print(cat['name'])
        new_cat = Category.objects.create(name=cat['name'], slug=cat['slug'], order=cat['order'])


def get_parent(parent_string):
    parents = parent_string.split('/')
    nodes = Category.objects.root_nodes()
    for p in parents:
        parent = nodes.filter(slug=p)
        nodes = parent.get_descendants()
    return parent.first()


def get_image_path(name):
    images_dir = 'images'
    current_dir = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(current_dir, images_dir, name)


print('СОЗДАНИЕ ТОВАРОВ:')
for product in products:
    print(product['name'])
    new_prod = Product.objects.create(name=product['name'],
                                      slug=product['slug'],
                                      price=product['price'],
                                      parent=get_parent(product['parent']),
                                      sizes=OrderedDict(product['sizes']),
                                      vendor_code=product['vendor_code'],
                                      )
    image = get_image_path(product['image'])
    image_data = open(image, 'rb')
    django_file = File(image_data)
    new_prod.image.save(product['image'], django_file, save=True)

Answer:

Thank you all for your participation! It turned out that the product model should not be inherited from MPTTModel, but from models.Model, and everything becomes fine

Scroll to Top